Repository navigation
v1.1.0 feat: add video generation support with multi-provider runtime - #227
Conversation
fix(deps): update dependency react-i18next to ^16.5.4
fix(deps): update react ecosystem to ^19.2.4
fix(deps): update ai sdk packages
…runtime(need more fix)
fix(deps): update dependency better-auth to ^1.4.18
fix(deps): update dependency framer-motion to ^12.29.2
fix(deps): update dependency @lobehub/ui to ^4.32.2
chore(deps): update pnpm to v10.28.2
chore(deps): update dependency @biomejs/biome to ^2.3.13
fix(deps): update ai sdk packages
fix(deps): update dependency zustand to v5.0.11
feat: add video generation support with multi-provider runtime
WalkthroughThis PR introduces comprehensive video generation support across the application stack. It adds a new Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Arena
participant API as /api/workflow/video-stream
participant Provider as Video Provider<br/>(OpenAI/Google/etc)
participant Storage as Storage Service
User->>Arena: Submit video prompt
Arena->>Arena: Validate input mode = videoGenerate
Arena->>API: POST request with prompt<br/>(SSE stream)
API->>API: Authenticate user
API->>Provider: Initialize video client<br/>& create generation task
Provider-->>API: Task ID
loop Heartbeat & Poll
API->>API: Emit heartbeat event
API->>Provider: Poll task status
Provider-->>API: Progress/Status
API-->>Arena: Stream event (heartbeat/progress)
end
Provider-->>API: Task complete, video URL
API->>Storage: Download & re-upload video
Storage-->>API: Signed download URL
API-->>Arena: Video completion event<br/>(url, storage path)
Arena->>Arena: Display video in chat
Arena->>API: Save video attachment<br/>via onSaveModelResponse
API->>API: Persist video metadata
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/sidebar.tsx (1)
162-220:⚠️ Potential issue | 🟡 MinorAdd an explicit accessible label for the expand button and mark the logos decorative.
The button’s label currently depends on the logo image alt text (“lmring”) and disappears when hover swaps to the icon. Add an explicit
aria-labeland make the images decorative to keep a stable, action-oriented accessible name.♿ Proposed accessibility fix
- <button + <button type="button" className="flex items-center justify-center w-full h-full hover:bg-sidebar-accent transition-colors rounded-lg" onClick={() => setCollapsed(false)} onMouseEnter={() => setIsLogoHovered(true)} onMouseLeave={() => setIsLogoHovered(false)} + aria-label="Expand sidebar" > {isLogoHovered ? ( <PanelLeftOpen className="h-6 w-6 text-primary" /> ) : ( <> <Image src="/athena-black.svg" - alt="lmring" + alt="" + aria-hidden="true" width={24} height={24} className="dark:hidden" style={{ width: 24, height: 24, flexShrink: 0 }} /> <Image src="/athena-white.svg" - alt="lmring" + alt="" + aria-hidden="true" width={24} height={24} className="hidden dark:block" style={{ width: 24, height: 24, flexShrink: 0 }} /> </> )} </button><motion.div initial={{ opacity: 0, width: 0 }} animate={{ opacity: 1, width: 'auto' }} exit={{ opacity: 0, width: 0 }} transition={{ duration: 0.2 }} className="flex items-center gap-2 whitespace-nowrap overflow-hidden" > <Image src="/athena-black.svg" - alt="lmring" + alt="" + aria-hidden="true" width={24} height={24} className="dark:hidden" style={{ width: 24, height: 24, flexShrink: 0 }} /> <Image src="/athena-white.svg" - alt="lmring" + alt="" + aria-hidden="true" width={24} height={24} className="hidden dark:block" style={{ width: 24, height: 24, flexShrink: 0 }} /> <span className="text-lg font-semibold">lmring</span> </motion.div>
🤖 Fix all issues with AI agents
In `@apps/web/src/components/arena/arena-hero-section.tsx`:
- Around line 30-34: The heroTitleKey is chosen with Math.random during render
which causes server/client hydration mismatches; change
HERO_TITLE_KEYS[Math.random...] logic to use useState to set a deterministic
initial value (e.g., index 0 or a stable hash) and then update the state to a
random selection inside useEffect after mount, referencing heroTitleKey and
HERO_TITLE_KEYS so the randomization happens post-hydration; also add an
explicit return type to the ArenaHeroSection component (e.g., React.FC or
JSX.Element) to satisfy TypeScript guidelines.
In `@apps/web/src/components/arena/video-response.tsx`:
- Around line 11-35: The VideoResponse function is missing an explicit return
type; update its signature to declare the return type (e.g. export function
VideoResponse({...}: VideoResponseProps): JSX.Element | null) so TypeScript
knows this component returns a JSX element or null; modify the VideoResponse
declaration accordingly (referencing VideoResponse and VideoResponseProps) and
ensure any necessary JSX types are available in the file.
In `@apps/web/src/hooks/use-workflow-execution.ts`:
- Around line 439-537: executeVideoWorkflowStream currently leaves
setVideoGenerating(id, true) uncleared on error/failure and only triggers
createConversationOnFirstChunk() when a 'heartbeat' arrives; fix by (1) adding a
finally block after the try/catch that always calls setVideoGenerating(id,
false) and setAbortController(id, undefined) to reset UI state, and (2) moving
the "first-event" conversation creation logic (use
conversationCreationPromiseRef.current and createConversationOnFirstChunk()) out
of the heartbeat branch so that on the first non-error stream event (regardless
of type) you await/initialize the conversationCreationPromiseRef, set
dbMessageId and currentDbMessageIdRef from its result before handling
video/complete events to ensure persistence works for new conversations.
In `@packages/ai-hub/src/core/video/index.ts`:
- Around line 130-141: The VideoGenerationParams interface is too flat and
causes generateVideo (in packages/ai-hub/src/core/video/index.ts) to hardcode
type: 'text-to-video'; update the params type (in
packages/ai-hub/src/types/runtime.ts) to include an input field that matches the
runtime's VideoGenerationInput union (i.e., accept { type: 'text-to-video',
prompt: string } | { type: 'image-to-video', image: string | Buffer, ... }),
then change generateVideo to forward params.input (and params.input.type) into
the client.generate call instead of using a hardcoded type and top-level
prompt/width/height/duration fields so both text-to-video and image-to-video are
supported.
In `@packages/video-runtime/src/core/factory.ts`:
- Around line 33-46: createProvider currently builds cacheKey from provider and
baseURL only (cacheKey) so different configs (apiKey/headers/timeout) reuse the
same instance; update createProvider to compute a stable fingerprint of the
relevant config fields (e.g., API key, headers, timeouts — canonicalized/sorted
JSON or a small hash) and incorporate that fingerprint into cacheKey before
checking/setting this.cache, or alternatively bypass caching when config differs
from the cached instance; update references to cacheKey, instantiateProvider,
and this.cache accordingly so each distinct credential/config produces its own
cached provider.
In `@packages/video-runtime/src/providers/google.ts`:
- Around line 152-160: The resolveModelId function is incorrectly mapping
'veo-3' and 'veo-3.0' to the Veo 2.0 model; update resolveModelId (and use
stripProviderPrefix) so that when stripped equals 'veo-3' or 'veo-3.0' it
returns the correct Veo 3 model ID (e.g., 'veo-3.0-generate-001' or choose the
fast variant 'veo-3.0-fast-generate-001' as appropriate), leaving the existing
Veo 2.0 mappings for 'veo-2'/'veo-2.0' unchanged. Ensure the change only affects
the conditional branches in resolveModelId so Veo 3 requests are routed to the
proper Gemini model ID.
In `@packages/video-runtime/src/providers/minimax.ts`:
- Around line 124-155: mapQualityToResolution includes 'pro'/'master' but
MiniMaxVideoProvider.capabilities.qualityTiers does not, causing those branches
to be unreachable; update the capabilities to include 'pro' and 'master' in
qualityTiers (or remove the 'pro'/'master' cases in mapQualityToResolution) so
validation and mapping align, specifically modify
MiniMaxVideoProvider.capabilities.qualityTiers to add 'pro' and 'master' if you
want to expose those higher tiers.
In `@packages/video-runtime/src/providers/openai.ts`:
- Around line 116-145: The VideoProvider currently advertises qualityTiers but
ignores params.quality; update resolveModelId (or createTask) to map quality
into the model name: if params.quality === 'high' select 'sora-2-pro', if
'standard' select 'sora-2' (while still allowing explicit provider-prefixed
modelIds), or alternatively remove qualityTiers from the capabilities object;
specifically modify resolveModelId(modelId: string) to accept an optional
quality hint (or read params.quality before calling resolveModelId in
createTask) and return 'sora-2-pro' for high and 'sora-2' for standard, keeping
existing stripProviderPrefix behavior for explicit model names.
In `@README.md`:
- Line 2: In README.md replace the white-logo reference by updating the image
src "apps/web/public/athena-white.svg" to "apps/web/public/athena-black.svg"
(keep the existing <img> attributes and alt text) so the logo is visible on
light backgrounds; verify the <h1> line still renders correctly and commit the
updated README.
🟡 Minor comments (12)
apps/web/public/language-sw.js-78-86 (1)
78-86:⚠️ Potential issue | 🟡 MinorInclude video extensions in the static-asset skip list.
The new guard is intended to bypass language headers for static assets, but common video formats (now part of the product) are still processed. If same-origin videos are served from
/publicor/static, this will fragment caches and may complicate range requests. Consider extending the regex.🔧 Suggested update
- pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map)$/i) || + pathname.match(/\.(svg|png|jpg|jpeg|gif|webp|ico|css|js|woff|woff2|ttf|eot|map|mp4|webm|mov|m4v)$/i) ||apps/web/src/components/sidebar.test.tsx-70-73 (1)
70-73:⚠️ Potential issue | 🟡 MinorAlign the settings-page collapse assertion with the new lowercase branding.
The title now renders as
lmring, but the settings-page test still checks forLMRing, which can pass even if the title is mistakenly visible. Update the assertion to match the new casing (or use a case-insensitive check).🔧 Proposed test fix
- expect(screen.queryByText('LMRing')).not.toBeInTheDocument(); + expect(screen.queryByText('lmring')).not.toBeInTheDocument();Also applies to: 208-214
apps/web/src/components/arena/video-player.tsx-45-46 (1)
45-46:⚠️ Potential issue | 🟡 MinorCheck response status before processing blob.
response.blob()will proceed even on non-2xx responses (e.g., 403, 404), resulting in downloading an error page as a video file.🛡️ Proposed fix to validate response
fetch(url) - .then((response) => response.blob()) + .then((response) => { + if (!response.ok) { + throw new Error(`Download failed: ${response.status}`); + } + return response.blob(); + })apps/web/src/components/arena/video-player.tsx-43-56 (1)
43-56:⚠️ Potential issue | 🟡 MinorAdd error handling for the download fetch.
The download handler silently fails if the fetch request fails (network error, CORS, 404, etc.). Users won't know why the download didn't work.
🛡️ Proposed fix with error handling
listeners: { download: (event: Event) => { event.preventDefault(); fetch(url) .then((response) => response.blob()) .then((blob) => { const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = 'video.mp4'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); - }); + }) + .catch((error) => { + console.error('Failed to download video:', error); + }); }, },apps/web/src/app/(landing)/page.tsx-56-63 (1)
56-63:⚠️ Potential issue | 🟡 MinorAdd
aria-hidden="true"to the decorative glow layer to prevent screen readers from announcing the tagline twice.The duplicated
{t('Index.tagline')}text renders in both the glow layer (line 58) and the main text layer (line 62). Since the glow layer is purely decorative (using absolute positioning and blur), it should be hidden from assistive technology.🔧 Suggested change
- <span className="absolute inset-0 bg-gradient-to-r from-indigo-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent opacity-70 blur-sm"> + <span + aria-hidden="true" + className="absolute inset-0 bg-gradient-to-r from-indigo-500 via-blue-500 to-indigo-500 bg-clip-text text-transparent opacity-70 blur-sm" + > {t('Index.tagline')} </span>apps/web/src/components/arena/chat/video-attachment.tsx-30-31 (1)
30-31:⚠️ Potential issue | 🟡 MinorLocalize the “Generating video…” label.
This is user-facing text; please move it to the i18n dictionary and add keys for en/fr/zh.As per coding guidelines: Translate all three locale files (en, fr, zh) when adding new i18n keys during development.
apps/web/src/app/api/workflow/video-stream/route.ts-114-125 (1)
114-125:⚠️ Potential issue | 🟡 MinorUse provider
mimeTypefor upload content type and filename.
Hardcodingvideo/mp4and the.mp4extension can mislabel webm/other outputs and break playback or caching.🛠️ Suggested fix
- const storage = createStorageService(); - const timestamp = Date.now(); - const randomId = crypto.randomUUID().slice(0, 8); - storagePath = `users/${session.user.id}/videos/${timestamp}-${randomId}.mp4`; + const storage = createStorageService(); + const timestamp = Date.now(); + const randomId = crypto.randomUUID().slice(0, 8); + const mimeType = event.video.mimeType ?? 'video/mp4'; + const extension = + mimeType.split('/')[1]?.split(';')[0]?.trim() || 'mp4'; + storagePath = `users/${session.user.id}/videos/${timestamp}-${randomId}.${extension}`; - await storage.upload(storagePath, videoBuffer, { - contentType: 'video/mp4', - }); + await storage.upload(storagePath, videoBuffer, { + contentType: mimeType, + });apps/web/src/app/api/workflow/video-stream/route.ts-30-30 (1)
30-30:⚠️ Potential issue | 🟡 MinorAdd an explicit return type for the POST handler.
♻️ Suggested refactor
-export async function POST(request: Request) { +export async function POST(request: Request): Promise<Response> {Async Next.js API route handlers should explicitly declare their return type as
Promise<Response>to comply with TypeScript best practices and coding guidelines.apps/web/src/stores/workflow-store.tsx-556-581 (1)
556-581:⚠️ Potential issue | 🟡 MinorPotential state inconsistency when stopping video generation.
When
setVideoGenerating(id, false)is called,pendingResponseis cleared butstatusis preserved viaworkflow.status(line 572). If the workflow was in'running'state, it remains'running'even though there's no pending response. This could leave the workflow in an inconsistent state.Consider explicitly setting the status when stopping video generation, similar to how
completePendingResponsesetsstatus: 'completed'.🔧 Proposed fix
setVideoGenerating: (id, isGenerating) => { set( (state) => { const workflow = state.workflows.get(id); if (!workflow) return state; const newMap = new Map(state.workflows); newMap.set(id, { ...workflow, pendingResponse: isGenerating ? { content: '', startTime: Date.now(), isVideoGenerating: true, } : undefined, - status: isGenerating ? 'running' : workflow.status, + status: isGenerating ? 'running' : 'pending', error: isGenerating ? undefined : workflow.error, updatedAt: new Date(), }); return { workflows: newMap }; },packages/video-runtime/src/providers/base.ts-108-142 (1)
108-142:⚠️ Potential issue | 🟡 MinorReject non-positive durations to avoid invalid requests.
params.durationis only checked when greater than the max; 0 or negative durations pass validation and can trigger provider errors. Add an explicit validation for<= 0.🛡️ Suggested validation
- if (params.duration && params.duration > this.capabilities.maxDurationSeconds) { + if (params.duration !== undefined && params.duration <= 0) { + errors.push('Duration must be greater than 0 seconds'); + } + if (params.duration !== undefined && params.duration > this.capabilities.maxDurationSeconds) { errors.push( `Duration ${params.duration}s exceeds maximum ${this.capabilities.maxDurationSeconds}s`, ); }packages/video-runtime/src/utils/http.ts-90-106 (1)
90-106:⚠️ Potential issue | 🟡 MinorAvoid JSON-stringifying non-JSON request bodies.
httpRequestalwaysJSON.stringifys any provided body, which breaks if callers pass a string,FormData, or otherBodyInittypes. This also forcesContent-Type: application/jsoneven for non-JSON requests.🐛 Safer body handling
- const requestHeaders: Record<string, string> = { - 'Content-Type': 'application/json', - Accept: 'application/json', - ...headers, - }; + const isJsonBody = + body !== undefined && + typeof body === 'object' && + !(body instanceof FormData) && + !(body instanceof URLSearchParams); + + const requestHeaders: Record<string, string> = { + ...(isJsonBody ? { 'Content-Type': 'application/json' } : {}), + Accept: 'application/json', + ...headers, + }; ... - body: body !== undefined ? JSON.stringify(body) : undefined, + body: body === undefined ? undefined : isJsonBody ? JSON.stringify(body) : (body as BodyInit),packages/video-runtime/src/providers/google.ts-209-233 (1)
209-233:⚠️ Potential issue | 🟡 MinorHandle synchronous completion without dropping the video result.
If Google ever returns a synchronous
doneresponse, you return"sync-completed"but never preserve the actual video payload—pollTaskStatusthen yieldscompletedwithresult: undefined. That can surface as “success with no video”.💡 One possible fix (stash the sync result)
export class GoogleVideoProvider extends BaseVideoProvider { + private syncResult?: VideoGenerationResult; ... protected async createTask(...) { ... if (!response.data.name) { if (response.data.done && response.data.response) { - return 'sync-completed'; + this.syncResult = extractVideoResult(response.data); + return 'sync-completed'; } throw VideoError.providerError(...); } return response.data.name; } protected async pollTaskStatus(...) { if (taskId === 'sync-completed') { return { taskId, - status: 'completed', - result: undefined, + status: this.syncResult ? 'completed' : 'failed', + result: this.syncResult, }; } ... } }
🧹 Nitpick comments (30)
packages/ai-hub/src/types/runtime.ts (1)
115-118: Consider adding aprogressevent variant for video generation feedback.The PR objectives mention "SSE-based video streaming workflow API for progress updates," but the current union only includes
heartbeat,video, anderrorvariants. Since video generation can be a long-running operation, aprogressevent would improve the user experience by providing feedback during generation.💡 Suggested enhancement
export type VideoStreamEvent = | { type: 'heartbeat' } + | { type: 'progress'; progress: number; message?: string } | { type: 'video'; video: VideoGenerationResult } | { type: 'error'; error: string };apps/web/src/app/api/conversations/[id]/full/route.ts (1)
9-26: Consider extractingattachmentsWithUrlsto a shared utility.This function is duplicated verbatim in
apps/web/src/app/api/model-responses/route.ts. To adhere to DRY principles and simplify maintenance, consider extracting it to a shared location (e.g.,@/libs/attachmentsor within@lmring/storage).♻️ Example shared utility
Create a new file
apps/web/src/libs/attachments.ts:import type { ResponseAttachment } from '@lmring/database/schema'; import { createStorageService } from '@lmring/storage'; /** * Convert attachment storage keys to signed URLs * If an attachment already has an external URL (e.g., video URLs), use that directly */ export async function attachmentsWithUrls( attachments: ResponseAttachment[] | null, ): Promise<Array<ResponseAttachment & { url: string }> | null> { if (!attachments || attachments.length === 0) return null; const storage = createStorageService(); return Promise.all( attachments.map(async (att) => ({ ...att, url: att.url || (await storage.createDownloadUrl(att.key)), })), ); }Then import from both route files:
import { attachmentsWithUrls } from '@/libs/attachments';apps/web/src/app/layout.tsx (1)
21-23: Use an interface and explicit return type forRootLayout.Inline object types and implicit async return types violate TS style guidance for this repo. Consider:
♻️ Suggested refactor
+interface RootLayoutProps { + children: React.ReactNode; +} + -export default async function RootLayout(props: { children: React.ReactNode }) { +export default async function RootLayout( + props: RootLayoutProps +): Promise<React.ReactElement> {As per coding guidelines, Prefer interfaces over types for object shapes in TypeScript; Use explicit return types for functions in TypeScript.
apps/web/src/components/arena/vote-bar.test.tsx (1)
54-57: Consider adding explicitdisabled={false}to clarify test intent.The test verifies that
VoteBarrenders nothing, but doesn't explicitly passdisabled={false}. Since the implementation unconditionally returnsnull(regardless of thedisabledprop), consider making this explicit in the test to document that voting is disabled at the implementation level, not via props.it('should render nothing when voting is disabled', () => { - const { container } = render(<VoteBar {...defaultProps} />); + const { container } = render(<VoteBar {...defaultProps} disabled={false} />); expect(container.firstChild).toBeNull(); });apps/web/src/components/arena/vote-bar.tsx (3)
27-35: Add explicit return type forVoteBarcomponent.The component lacks an explicit return type annotation. As per coding guidelines, TypeScript functions should have explicit return types.
+// VOTE_DISABLED: VoteBar component temporarily disabled export const VoteBar = React.memo(function VoteBar({ messageId: _messageId, modelResponses: _modelResponses, comparisonType: _comparisonType, disabled: _disabled = false, -}: VoteBarProps) { +}: VoteBarProps): React.ReactNode { // Return null to hide the vote bar while voting is disabled return null;
37-192: Consider extracting commented implementation to a separate branch or using feature flags.The large block of commented-out code (150+ lines) increases maintenance burden and risks becoming stale. Consider:
- Keeping the original implementation in a separate git branch for reference
- Using a feature flag/environment variable to toggle voting at runtime
- If this is truly temporary, adding a TODO with a target date/issue reference for re-enablement
This would keep the codebase cleaner while preserving the ability to restore functionality.
195-208: Add explicit return type foruseCardVotinghook.The hook lacks an explicit return type annotation.
+interface CardVotingHandlers { + handleCardClick: (modelResponseId: string) => Promise<void>; + handleCardHover: (modelResponseId: string) => void; + handleCardHoverLeave: () => void; + isVoted: boolean; + isSubmitting: boolean; +} + // VOTE_DISABLED: useCardVoting hook temporarily disabled export function useCardVoting( _messageId: string, _comparisonType: ComparisonType, _participantIds: string[], -) { +): CardVotingHandlers { // Return no-op handlers while voting is disabled return {apps/web/src/components/arena/model-card.tsx (1)
135-147: Consider removing or commenting out unused vote state computations.
effectiveVoteStateis computed but only used to look up empty strings fromvoteStateBorderStyles. This computation could be removed along with the border styles object while voting is disabled:- const effectiveVoteState = hoverState || voteState; - - // VOTE_DISABLED: Vote state border styles temporarily disabled - const voteStateBorderStyles: Record<string, string> = { - // winner: 'ring-1 ring-amber-500 border-amber-500 shadow-amber-500/20', - winner: '', - loser: '', - // tie: 'ring-1 ring-green-500 border-green-500 shadow-green-500/20', - tie: '', - // all_bad: 'ring-1 ring-red-500 border-red-500 shadow-red-500/20', - all_bad: '', - none: '', - }; + // VOTE_DISABLED: Vote state border styles temporarily disabled + // const effectiveVoteState = hoverState || voteState; + // const voteStateBorderStyles: Record<string, string> = { + // winner: 'ring-1 ring-amber-500 border-amber-500 shadow-amber-500/20', + // loser: '', + // tie: 'ring-1 ring-green-500 border-green-500 shadow-green-500/20', + // all_bad: 'ring-1 ring-red-500 border-red-500 shadow-red-500/20', + // none: '', + // };And update
cardClassesto remove the lookup:const cardClasses = cn( 'h-full min-h-0 arena-card flex flex-col glass-effect relative overflow-hidden transition-all duration-200', - voteStateBorderStyles[effectiveVoteState], // VOTE_DISABLED: Vote hover styles temporarily disabled // isVotable && voteState === 'none' && 'cursor-pointer hover:ring-1 hover:ring-amber-500/50', );apps/web/src/components/arena/video-player.tsx (1)
17-17: Add explicit return type to the component function.Per coding guidelines, functions should have explicit return types. Consider adding
React.JSX.ElementorJSX.Elementas the return type.-export function VideoPlayer({ url, thumbnailUrl, className }: VideoPlayerProps) { +export function VideoPlayer({ url, thumbnailUrl, className }: VideoPlayerProps): React.JSX.Element {apps/web/src/libs/video-workflow-api.ts (1)
73-102: Consider flushing the TextDecoder after the read loop.When
doneis true, there may be remaining bytes in the decoder's internal buffer that need to be flushed. Additionally, any remaining content inbufferafter the loop should be processed.♻️ Suggested improvement
try { while (true) { const { done, value } = await reader.read(); if (done) { + // Flush decoder and process any remaining buffer + buffer += decoder.decode(); + if (buffer.trim() && buffer.startsWith('data: ')) { + const data = buffer.slice(6).trim(); + if (data && data !== '[DONE]') { + try { + const event = JSON.parse(data) as WorkflowStreamEvent; + yield event; + } catch { + console.error('Failed to parse final SSE event:', data); + } + } + } break; } buffer += decoder.decode(value, { stream: true });apps/web/src/components/landing/animated-hero.tsx (1)
7-13: Consider usinginterfacefor props definition.As per coding guidelines, prefer interfaces over types for object shapes in TypeScript. This is a minor style consideration.
♻️ Suggested change
-type AnimatedHeroProps = { +interface AnimatedHeroProps { title: ReactNode; description: ReactNode; actions: ReactNode; badge?: ReactNode; logo?: ReactNode; -}; +}packages/video-runtime/src/utils/errors.ts (2)
51-92: Consider case-insensitive matching for error message patterns.The string matching uses a mix of lowercase patterns (
'timeout','network') and uppercase ('ETIMEDOUT','ECONNREFUSED'). Consider normalizing to case-insensitive checks for more robust error classification.♻️ Suggested improvement
if (error instanceof Error) { // Check for common HTTP error patterns - const message = error.message; + const message = error.message.toLowerCase(); - if (message.includes('timeout') || message.includes('ETIMEDOUT')) { + if (message.includes('timeout') || message.includes('etimedout')) { return new VideoError({ code: 'TIMEOUT', - message: `Request timeout: ${message}`, + message: `Request timeout: ${error.message}`, retryable: true, cause: error, ...defaults, }); } - if (message.includes('network') || message.includes('ECONNREFUSED')) { + if (message.includes('network') || message.includes('econnrefused')) { return new VideoError({ code: 'NETWORK_ERROR', - message: `Network error: ${message}`, + message: `Network error: ${error.message}`, retryable: true, cause: error, ...defaults, }); }
224-229: Consider extending retryability check for non-VideoError instances.The current implementation returns
falsefor any non-VideoError. Consider checking for common retryable patterns in standard errors (e.g., network errors, timeouts) to make the utility more broadly useful.♻️ Alternative approach
export function isRetryableError(error: unknown): boolean { if (error instanceof VideoError) { return error.retryable; } + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes('timeout') || + message.includes('etimedout') || + message.includes('econnrefused') || + message.includes('rate limit') || + message.includes('429') + ); + } return false; }packages/video-runtime/src/utils/validation.ts (2)
70-82: Tie the schema type toVideoGenerationParamsto avoid unsafe casts.
Casting inparseParamscan mask drift between the Zod schema and the TS type.🔧 Suggested change
-export const VideoGenerationParamsSchema = z.object({ +export const VideoGenerationParamsSchema: z.ZodType<VideoGenerationParams> = z.object({ model: z.string().min(1, 'Model is required'), input: VideoGenerationInputSchema, width: z.number().int().positive().optional(), height: z.number().int().positive().optional(), aspectRatio: VideoAspectRatioSchema.optional(), duration: z.number().positive().max(300).optional(), fps: z.number().int().positive().max(120).optional(), quality: VideoQualitySchema.optional(), audio: z.boolean().optional(), seed: z.number().int().nonnegative().optional(), providerOptions: z.record(z.string(), z.unknown()).optional(), }); @@ export function parseParams(params: unknown): VideoGenerationParams { - return VideoGenerationParamsSchema.parse(params) as VideoGenerationParams; + return VideoGenerationParamsSchema.parse(params); }Also applies to: 116-118
123-125: Use the sharedTextToVideoInputinterface for the type guard.
This keeps the predicate aligned with the canonical input type.🔧 Suggested change
-import type { ImageToVideoInput, ValidationResult, VideoGenerationParams } from '../types'; +import type { ImageToVideoInput, TextToVideoInput, ValidationResult, VideoGenerationParams } from '../types'; @@ export function isTextToVideoInput( input: VideoGenerationParams['input'], -): input is { type: 'text-to-video'; prompt: string; negativePrompt?: string } { +): input is TextToVideoInput { return input.type === 'text-to-video'; }As per coding guidelines: Prefer interfaces over types for object shapes in TypeScript.
apps/web/src/app/(landing)/page.test.tsx (1)
7-13: Prefervi.spyOnfor these mocks.
This aligns with the testing guideline and keeps module shapes intact.🔧 Example refactor
-import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as i18n from '@lmring/i18n'; +import * as serverTranslations from '@/libs/server-translations'; @@ -vi.mock('@lmring/i18n', () => ({ - isValidLocale: (locale: string) => locale === 'en' || locale === 'fr', -})); - -vi.mock('@/libs/server-translations', () => ({ - getServerTranslations: async (locale: string) => (key: string) => `${locale}:${key}`, -})); +beforeEach(() => { + vi.spyOn(i18n, 'isValidLocale').mockImplementation( + (locale: string) => locale === 'en' || locale === 'fr', + ); + vi.spyOn(serverTranslations, 'getServerTranslations').mockResolvedValue( + (key: string) => `en:${key}`, + ); +});As per coding guidelines: Prefer vi.spyOn over vi.mock in test files.
apps/web/src/components/arena/chat/chat-list.tsx (1)
19-28: Add an explicit return type for the component.🔧 Suggested change
-export function ChatList({ +export function ChatList({ messages, pendingResponse, isLoading, status, error, providerId, onRetry, onMaximize, -}: ChatListProps) { +}: ChatListProps): JSX.Element {As per coding guidelines: Use explicit return types for functions in TypeScript.
apps/web/src/components/arena/arena-hero-section.tsx (1)
27-27: Add an explicit return type for the component.🔧 Suggested change
-export function ArenaHeroSection() { +export function ArenaHeroSection(): JSX.Element {As per coding guidelines: Use explicit return types for functions in TypeScript.
apps/web/src/app/(landing)/page.tsx (2)
17-19: Prefer an interface forIIndexProps.🔧 Suggested change
-type IIndexProps = { +interface IIndexProps { params: Promise<{ locale: string }>; -}; +}As per coding guidelines: Prefer interfaces over types for object shapes in TypeScript.
32-35: Add an explicit return type for the page component.🔧 Suggested change
-export default async function Index(props: IIndexProps) { +export default async function Index(props: IIndexProps): Promise<JSX.Element> {As per coding guidelines: Use explicit return types for functions in TypeScript.
apps/web/src/components/arena/chat/video-attachment.tsx (1)
13-22: Add explicit return types to the exported components.♻️ Suggested refactor
-export function VideoAttachmentDisplay({ data, className }: VideoAttachmentProps) { +export function VideoAttachmentDisplay({ + data, + className, +}: VideoAttachmentProps): JSX.Element { return ( <div className={cn('w-[480px] rounded-lg border bg-muted', className)}> <VideoPlayer url={data.url} thumbnailUrl={data.thumbnailUrl} className="w-full" /> </div> ); } -export function VideoGeneratingIndicator({ className }: { className?: string }) { +export function VideoGeneratingIndicator({ + className, +}: { + className?: string; +}): JSX.Element { return ( <div className={cn( 'flex items-center gap-2 p-4 rounded-lg border bg-muted/30 w-[350px]', className, )} >Both
VideoAttachmentDisplayandVideoGeneratingIndicatorlack explicit return types. Per TypeScript best practices and project guidelines, add: JSX.Elementto both functions.packages/model-depot/src/models/togetherai.ts (1)
323-342: Consider addingmaxDurationSecondsfor consistency.The Wan-AI models (
Wan2.2-I2V-A14BandWan2.2-T2V-A14B) are missingmaxDurationSeconds,resolutions, andfpsproperties that other video models in this file have. If these values are known, adding them would improve consistency and enable better validation in the video runtime.💡 Example for consistency
{ id: 'Wan-AI/Wan2.2-I2V-A14B', displayName: 'Wan 2.2 Image-to-Video', description: 'Wan-AI image-to-video generation model', type: 'video', enabled: false, abilities: { videoOutput: true }, + maxDurationSeconds: 5, + resolutions: ['1280x720', '720x1280'], + fps: 24, organization: 'Wan-AI', runtimeProvider: 'openai-compatible', },packages/video-runtime/src/providers/kling.ts (1)
144-154: Minor: Simplify model resolution logic.The
resolveModelIdmethod has redundant branches. The finalreturn strippedat line 153 handles thestartsWith('kling-')case already.♻️ Simplified version
resolveModelId(modelId: string): string { const stripped = stripProviderPrefix(modelId); // Map common names to Kling model IDs if (stripped === 'kling' || stripped === 'kling-v2') { return 'kling-v2-master'; } - if (stripped.startsWith('kling-')) { - return stripped; - } return stripped; }packages/ai-hub/src/core/video/index.ts (1)
61-71: Redundant regex pattern inisOfficialGoogleEndpoint.The second pattern
/^https:\/\/.*\.googleapis\.com/is a superset of the first pattern/^https:\/\/generativelanguage\.googleapis\.com/, making the first pattern redundant. Consider simplifying or documenting the intent if the specificity is deliberate.♻️ Suggested simplification
const officialPatterns = [ - /^https:\/\/generativelanguage\.googleapis\.com/, /^https:\/\/.*\.googleapis\.com/, /^https:\/\/aiplatform\.googleapis\.com/, ];packages/model-depot/src/types/model.ts (1)
127-135: Consider adding video-specific fields toFullModelCardfor completeness.
FullModelCardserves as a unified type but lacks video-specific fields likemaxDurationSecondsandfpsthat exist inVideoModelCard. This could lead to data loss when converting fromVideoModelCardtoFullModelCard.♻️ Proposed addition to FullModelCard
export interface FullModelCard extends BaseModelCard { type: AiModelType; abilities?: ModelAbilities; pricing?: ModelPricing; maxDimension?: number; resolutions?: string[]; + maxDurationSeconds?: number; + fps?: number; runtimeProvider?: string; runtimeModelId?: string; }packages/video-runtime/src/core/factory.ts (1)
84-103: DRY the supported-provider list to prevent drift.
isSupportedandgetSupportedProviderseach define their own list. A shared constant avoids inconsistencies when providers are added/removed.♻️ Suggested refactor
+const SUPPORTED_PROVIDERS: VideoRuntimeProvider[] = [ + 'openai', + 'minimax', + 'google', + 'kling', + 'seedance', + 'vidu', + 'openai-compatible', +]; + isSupported(provider: VideoRuntimeProvider): boolean { - const supportedProviders: VideoRuntimeProvider[] = [ - 'openai', - 'minimax', - 'google', - 'kling', - 'seedance', - 'vidu', - 'openai-compatible', - ]; - return supportedProviders.includes(provider); + return SUPPORTED_PROVIDERS.includes(provider); } getSupportedProviders(): VideoRuntimeProvider[] { - return ['openai', 'minimax', 'google', 'kling', 'seedance', 'vidu', 'openai-compatible']; + return [...SUPPORTED_PROVIDERS]; }packages/video-runtime/src/providers/openai-compatible.ts (1)
53-72: Align response typing with actualdatashapes.
extractVideoUrlhandles both array and object forms, butOpenAICompatibleVideoResponse.dataonly models an array, forcing casts. A union keeps type safety.♻️ Suggested typing update
interface OpenAICompatibleVideoResponse { id?: string; // OpenAI standard format task_id?: string; // MiniMax/bltcy.ai format request_id?: string; // Alternative format status: string; model?: string; created_at?: number; video_url?: string; url?: string; output?: string; // Veo API format: direct output field - data?: Array<{ - url: string; - revised_prompt?: string; - }>; + data?: + | Array<{ + url: string; + revised_prompt?: string; + }> + | { output?: string }; error?: { code: string; message: string; }; progress?: number; }packages/video-runtime/src/providers/openai.ts (1)
79-105: AlignaspectRatioToSizemappings with declared capabilities.
aspectRatioToSizeincludes21:9, butcapabilities.aspectRatiosdoesn't, so validation blocks it. Either add21:9to capabilities or drop the mapping to avoid dead code.♻️ Option: remove unsupported mapping
case '3:4': return '1080x1440'; - case '21:9': - return '2560x1080'; default: return undefined;apps/web/src/hooks/use-workflow-execution.ts (1)
439-597: Add explicit Promise return types to new callbacks.The new async callbacks don’t declare return types. Please annotate them to match the TypeScript guideline.
As per coding guidelines, Use explicit return types for functions in TypeScript.✍️ Suggested annotations
- const executeVideoWorkflowStream = useCallback( - async ( + const executeVideoWorkflowStream = useCallback( + async ( workflow: ArenaWorkflow, prompt: string, options?: { isNewConversation?: boolean; existingDbMessageId?: string; }, - ) => { + ): Promise<void> => { ... }, [...] ); - const startAllSyncedVideoWorkflows = useCallback(async () => { + const startAllSyncedVideoWorkflows = useCallback(async (): Promise<void> => { ... }, [...]);apps/web/src/app/(auth)/arena/[[...conversationId]]/page.tsx (1)
1046-1169: Add an explicit return type tohandleSubmit.The modified callback lacks a return type; per the TS guideline, consider annotating it as
Promise<void>.As per coding guidelines, Use explicit return types for functions in TypeScript.
| const heroTitleKey = useMemo(() => { | ||
| return ( | ||
| HERO_TITLE_KEYS[Math.floor(Math.random() * HERO_TITLE_KEYS.length)] ?? HERO_TITLE_KEYS[0] | ||
| ); | ||
| }, []); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "arena-hero-section.tsx" -type fRepository: llm-ring/lmring
Length of output: 113
🏁 Script executed:
cat -n apps/web/src/components/arena/arena-hero-section.tsxRepository: llm-ring/lmring
Length of output: 2104
🏁 Script executed:
head -50 apps/web/src/components/arena/arena-hero-section.tsx | cat -nRepository: llm-ring/lmring
Length of output: 1534
Avoid Math.random in render to prevent hydration mismatch.
Even though this is a client component ('use client'), it's still server-rendered initially. Using Math.random() during the render phase produces different values on the server and client, causing a hydration mismatch warning and potential UI flicker. Use useState with a deterministic initial value and set randomness in useEffect after hydration completes.
Also add an explicit return type to the ArenaHeroSection function per TypeScript guidelines.
🔧 Suggested change
-import { useMemo } from 'react';
+import { useEffect, useState } from 'react';
@@
- const heroTitleKey = useMemo(() => {
- return (
- HERO_TITLE_KEYS[Math.floor(Math.random() * HERO_TITLE_KEYS.length)] ?? HERO_TITLE_KEYS[0]
- );
- }, []);
+ const [heroTitleKey, setHeroTitleKey] = useState<(typeof HERO_TITLE_KEYS)[number]>(
+ HERO_TITLE_KEYS[0],
+ );
+
+ useEffect(() => {
+ setHeroTitleKey(
+ HERO_TITLE_KEYS[Math.floor(Math.random() * HERO_TITLE_KEYS.length)] ?? HERO_TITLE_KEYS[0],
+ );
+ }, []);🤖 Prompt for AI Agents
In `@apps/web/src/components/arena/arena-hero-section.tsx` around lines 30 - 34,
The heroTitleKey is chosen with Math.random during render which causes
server/client hydration mismatches; change HERO_TITLE_KEYS[Math.random...] logic
to use useState to set a deterministic initial value (e.g., index 0 or a stable
hash) and then update the state to a random selection inside useEffect after
mount, referencing heroTitleKey and HERO_TITLE_KEYS so the randomization happens
post-hydration; also add an explicit return type to the ArenaHeroSection
component (e.g., React.FC or JSX.Element) to satisfy TypeScript guidelines.
| export function VideoResponse({ url, thumbnailUrl, isLoading, error }: VideoResponseProps) { | ||
| if (error) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-2 p-6 text-destructive"> | ||
| <AlertCircle className="h-8 w-8" /> | ||
| <p className="text-sm text-center">{error}</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center gap-3 p-8"> | ||
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | ||
| <p className="text-sm text-muted-foreground">Generating video...</p> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!url) return null; | ||
|
|
||
| return ( | ||
| <div className="w-full flex items-center justify-center p-4"> | ||
| <VideoPlayer url={url} thumbnailUrl={thumbnailUrl} className="rounded-lg w-full max-w-2xl" /> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/src/components/arena/video-response.tsx | head -40Repository: llm-ring/lmring
Length of output: 1377
Add an explicit return type to VideoResponse.
Per TypeScript guidelines, the function should declare its return type explicitly.
Proposed change
-export function VideoResponse({ url, thumbnailUrl, isLoading, error }: VideoResponseProps) {
+export function VideoResponse(
+ { url, thumbnailUrl, isLoading, error }: VideoResponseProps,
+): JSX.Element | null {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function VideoResponse({ url, thumbnailUrl, isLoading, error }: VideoResponseProps) { | |
| if (error) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center gap-2 p-6 text-destructive"> | |
| <AlertCircle className="h-8 w-8" /> | |
| <p className="text-sm text-center">{error}</p> | |
| </div> | |
| ); | |
| } | |
| if (isLoading) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center gap-3 p-8"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | |
| <p className="text-sm text-muted-foreground">Generating video...</p> | |
| </div> | |
| ); | |
| } | |
| if (!url) return null; | |
| return ( | |
| <div className="w-full flex items-center justify-center p-4"> | |
| <VideoPlayer url={url} thumbnailUrl={thumbnailUrl} className="rounded-lg w-full max-w-2xl" /> | |
| </div> | |
| export function VideoResponse( | |
| { url, thumbnailUrl, isLoading, error }: VideoResponseProps, | |
| ): JSX.Element | null { | |
| if (error) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center gap-2 p-6 text-destructive"> | |
| <AlertCircle className="h-8 w-8" /> | |
| <p className="text-sm text-center">{error}</p> | |
| </div> | |
| ); | |
| } | |
| if (isLoading) { | |
| return ( | |
| <div className="flex flex-col items-center justify-center gap-3 p-8"> | |
| <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> | |
| <p className="text-sm text-muted-foreground">Generating video...</p> | |
| </div> | |
| ); | |
| } | |
| if (!url) return null; | |
| return ( | |
| <div className="w-full flex items-center justify-center p-4"> | |
| <VideoPlayer url={url} thumbnailUrl={thumbnailUrl} className="rounded-lg w-full max-w-2xl" /> | |
| </div> |
🤖 Prompt for AI Agents
In `@apps/web/src/components/arena/video-response.tsx` around lines 11 - 35, The
VideoResponse function is missing an explicit return type; update its signature
to declare the return type (e.g. export function VideoResponse({...}:
VideoResponseProps): JSX.Element | null) so TypeScript knows this component
returns a JSX element or null; modify the VideoResponse declaration accordingly
(referencing VideoResponse and VideoResponseProps) and ensure any necessary JSX
types are available in the file.
| const executeVideoWorkflowStream = useCallback( | ||
| async ( | ||
| workflow: ArenaWorkflow, | ||
| prompt: string, | ||
| options?: { | ||
| isNewConversation?: boolean; | ||
| existingDbMessageId?: string; | ||
| }, | ||
| ) => { | ||
| const { id, modelId, keyId } = workflow; | ||
| const { isNewConversation = false, existingDbMessageId } = options || {}; | ||
|
|
||
| addUserMessage(id, prompt); | ||
| setVideoGenerating(id, true); | ||
|
|
||
| const abortController = new AbortController(); | ||
| setAbortController(id, abortController); | ||
|
|
||
| const request = buildVideoStreamRequest(id, modelId, keyId, prompt); | ||
|
|
||
| let videoResult: VideoAttachment | undefined; | ||
| let metrics: WorkflowMetrics | undefined; | ||
| let dbMessageId = existingDbMessageId; | ||
| let isFirstEvent = true; | ||
|
|
||
| try { | ||
| for await (const event of streamVideoWorkflow(request, abortController.signal)) { | ||
| if (event.type === 'heartbeat') { | ||
| // Handle conversation creation on first event for new conversations | ||
| if (isFirstEvent && isNewConversation && persistenceCallbacks) { | ||
| isFirstEvent = false; | ||
|
|
||
| if (!conversationCreationPromiseRef.current) { | ||
| conversationCreationPromiseRef.current = createConversationOnFirstChunk(); | ||
| } | ||
|
|
||
| const result = await conversationCreationPromiseRef.current; | ||
| if (result) { | ||
| dbMessageId = result.messageId; | ||
| currentDbMessageIdRef.current = result.messageId; | ||
| } | ||
| } | ||
| } else if (event.type === 'video' && event.video) { | ||
| videoResult = { | ||
| url: event.video.url, | ||
| storagePath: event.video.storagePath, | ||
| mimeType: event.video.mimeType, | ||
| thumbnailUrl: event.video.thumbnailUrl, | ||
| }; | ||
| } else if (event.type === 'complete') { | ||
| metrics = event.metrics; | ||
| } else if (event.type === 'error') { | ||
| setWorkflowStatus(id, 'failed', event.error); | ||
| setAbortController(id, undefined); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| if (videoResult) { | ||
| completeVideoResponse(id, videoResult, metrics); | ||
| } else { | ||
| setWorkflowStatus(id, 'failed', 'No video received'); | ||
| } | ||
| setAbortController(id, undefined); | ||
|
|
||
| if (dbMessageId && persistenceCallbacks?.onSaveModelResponse && videoResult) { | ||
| const [providerName, modelName] = modelId.split(':'); | ||
| try { | ||
| await persistenceCallbacks.onSaveModelResponse( | ||
| id, | ||
| dbMessageId, | ||
| modelName || modelId, | ||
| providerName || 'unknown', | ||
| `[video](${videoResult.url})`, | ||
| undefined, | ||
| metrics?.totalTime, | ||
| // Pass video attachment for persistence | ||
| [ | ||
| { | ||
| type: 'video', | ||
| key: videoResult.storagePath || videoResult.url, // Use storagePath for storage key | ||
| mimeType: videoResult.mimeType, | ||
| url: videoResult.url, // External URL | ||
| }, | ||
| ], | ||
| ); | ||
| } catch (error) { | ||
| console.error('Failed to save video response:', error); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof Error && error.name === 'AbortError') { | ||
| setWorkflowStatus(id, 'cancelled'); | ||
| } else { | ||
| const errorMessage = error instanceof Error ? error.message : 'Unknown error'; | ||
| setWorkflowStatus(id, 'failed', errorMessage); | ||
| } | ||
| setAbortController(id, undefined); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "setVideoGenerating|completeVideoResponse" apps -g'*.ts' -g'*.tsx'Repository: llm-ring/lmring
Length of output: 1279
🏁 Script executed:
sed -n '550,625p' apps/web/src/stores/workflow-store.tsxRepository: llm-ring/lmring
Length of output: 2397
🏁 Script executed:
rg -n "setWorkflowStatus" apps/web/src/hooks/use-workflow-execution.ts -A 2 | head -30Repository: llm-ring/lmring
Length of output: 1124
🏁 Script executed:
sed -n '439,537p' apps/web/src/hooks/use-workflow-execution.tsRepository: llm-ring/lmring
Length of output: 3707
🏁 Script executed:
sed -n '1,100p' apps/web/src/stores/workflow-store.tsx | grep -A 20 "setWorkflowStatus"Repository: llm-ring/lmring
Length of output: 41
🏁 Script executed:
rg -n "setWorkflowStatus:" apps/web/src/stores/workflow-store.tsx -A 30Repository: llm-ring/lmring
Length of output: 2647
🏁 Script executed:
rg -n "pendingResponse" apps/web/src/stores/workflow-store.tsx | head -50Repository: llm-ring/lmring
Length of output: 1309
🏁 Script executed:
rg -n "event.type ===" apps/web/src/hooks/use-workflow-execution.ts | grep -A 2 -B 2 "heartbeat\|video\|complete"Repository: llm-ring/lmring
Length of output: 515
Reset video-generation state in all paths and trigger conversation creation on first event.
Two confirmed issues:
-
Generating state stuck in UI:
setVideoGenerating(id, true)(line 452) is never reset in error/failure paths. When an error event occurs (line 490), the catch block runs (line 513), or "No video received" is hit (line 500),setWorkflowStatus('failed')doesn't clearpendingResponsefor failed status (workflow-store.tsx line 378 only clears on 'cancelled'). AddsetVideoGenerating(id, false)in afinallyblock. -
Conversation creation heartbeat-dependent: Conversation setup only triggers on
heartbeatevents (line 466). If the stream emitsvideoorcompletefirst, the conversation won't be created, breaking persistence for new conversations. Move conversation creation outside the heartbeat condition.
Suggested fix
const executeVideoWorkflowStream = useCallback(
async (
workflow: ArenaWorkflow,
prompt: string,
options?: {
isNewConversation?: boolean;
existingDbMessageId?: string;
},
) => {
const { id, modelId, keyId } = workflow;
const { isNewConversation = false, existingDbMessageId } = options || {};
addUserMessage(id, prompt);
setVideoGenerating(id, true);
const abortController = new AbortController();
setAbortController(id, abortController);
const request = buildVideoStreamRequest(id, modelId, keyId, prompt);
let videoResult: VideoAttachment | undefined;
let metrics: WorkflowMetrics | undefined;
let dbMessageId = existingDbMessageId;
let isFirstEvent = true;
try {
for await (const event of streamVideoWorkflow(request, abortController.signal)) {
+ if (isFirstEvent && isNewConversation && persistenceCallbacks) {
+ isFirstEvent = false;
+ if (!conversationCreationPromiseRef.current) {
+ conversationCreationPromiseRef.current = createConversationOnFirstChunk();
+ }
+ const result = await conversationCreationPromiseRef.current;
+ if (result) {
+ dbMessageId = result.messageId;
+ currentDbMessageIdRef.current = result.messageId;
+ }
+ }
+
- if (event.type === 'heartbeat') {
- // Handle conversation creation on first event for new conversations
- if (isFirstEvent && isNewConversation && persistenceCallbacks) {
- isFirstEvent = false;
-
- if (!conversationCreationPromiseRef.current) {
- conversationCreationPromiseRef.current = createConversationOnFirstChunk();
- }
-
- const result = await conversationCreationPromiseRef.current;
- if (result) {
- dbMessageId = result.messageId;
- currentDbMessageIdRef.current = result.messageId;
- }
- }
- } else if (event.type === 'video' && event.video) {
+ if (event.type === 'video' && event.video) {
videoResult = {
url: event.video.url,
storagePath: event.video.storagePath,
mimeType: event.video.mimeType,
thumbnailUrl: event.video.thumbnailUrl,
};
} else if (event.type === 'complete') {
metrics = event.metrics;
} else if (event.type === 'error') {
setWorkflowStatus(id, 'failed', event.error);
setAbortController(id, undefined);
- return;
+ throw new Error(event.error);
}
}
if (videoResult) {
completeVideoResponse(id, videoResult, metrics);
} else {
setWorkflowStatus(id, 'failed', 'No video received');
}
setAbortController(id, undefined);
if (dbMessageId && persistenceCallbacks?.onSaveModelResponse && videoResult) {
const [providerName, modelName] = modelId.split(':');
try {
await persistenceCallbacks.onSaveModelResponse(
id,
dbMessageId,
modelName || modelId,
providerName || 'unknown',
`[video](${videoResult.url})`,
undefined,
metrics?.totalTime,
// Pass video attachment for persistence
[
{
type: 'video',
key: videoResult.storagePath || videoResult.url,
mimeType: videoResult.mimeType,
url: videoResult.url,
},
],
);
} catch (error) {
console.error('Failed to save video response:', error);
}
}
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
setWorkflowStatus(id, 'cancelled');
} else {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
setWorkflowStatus(id, 'failed', errorMessage);
}
setAbortController(id, undefined);
+ } finally {
+ setVideoGenerating(id, false);
}🤖 Prompt for AI Agents
In `@apps/web/src/hooks/use-workflow-execution.ts` around lines 439 - 537,
executeVideoWorkflowStream currently leaves setVideoGenerating(id, true)
uncleared on error/failure and only triggers createConversationOnFirstChunk()
when a 'heartbeat' arrives; fix by (1) adding a finally block after the
try/catch that always calls setVideoGenerating(id, false) and
setAbortController(id, undefined) to reset UI state, and (2) moving the
"first-event" conversation creation logic (use
conversationCreationPromiseRef.current and createConversationOnFirstChunk()) out
of the heartbeat branch so that on the first non-error stream event (regardless
of type) you await/initialize the conversationCreationPromiseRef, set
dbMessageId and currentDbMessageIdRef from its result before handling
video/complete events to ensure persistence works for new conversations.
| for await (const event of client.generate( | ||
| { | ||
| model: params.model, | ||
| input: { | ||
| type: 'text-to-video', | ||
| prompt: params.prompt, | ||
| }, | ||
| width: params.width, | ||
| height: params.height, | ||
| duration: params.duration, | ||
| }, | ||
| signal, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for VideoGenerationParams interface definition
rg -n "interface VideoGenerationParams|type VideoGenerationParams" packages/ai-hub/src/Repository: llm-ring/lmring
Length of output: 140
🏁 Script executed:
# Read the actual code at lines 130-141 in the video index file
cat -n packages/ai-hub/src/core/video/index.ts | sed -n '125,150p'Repository: llm-ring/lmring
Length of output: 735
🏁 Script executed:
# Find where VideoGenerationParams is imported/defined
rg -n "VideoGenerationParams" packages/ai-hub/src/ -B 2 -A 2Repository: llm-ring/lmring
Length of output: 1403
🏁 Script executed:
# Read the full VideoGenerationParams interface definition
cat -n packages/ai-hub/src/types/runtime.ts | sed -n '98,120p'Repository: llm-ring/lmring
Length of output: 695
🏁 Script executed:
# Search for video-runtime type definitions and input type support
rg -n "type.*to-video|'text-to-video'|'image-to-video'" packages/ --type ts --type tsxRepository: llm-ring/lmring
Length of output: 86
🏁 Script executed:
# Check the video-runtime package to understand client.generate signature
find packages -name "*.ts" -path "*/video-runtime/*" | head -20Repository: llm-ring/lmring
Length of output: 949
🏁 Script executed:
# Look at what types/inputs are supported by video-runtime
rg -n "input.*type|VideoInput|GenerateInput" packages/ --type ts --type tsx -A 2Repository: llm-ring/lmring
Length of output: 86
🏁 Script executed:
# Search for input types without tsx flag
rg -n "text-to-video|image-to-video|VideoInput" packages/video-runtime/src/types/Repository: llm-ring/lmring
Length of output: 790
🏁 Script executed:
# Read video-runtime types definition
cat -n packages/video-runtime/src/types/video.ts | head -100Repository: llm-ring/lmring
Length of output: 3636
🏁 Script executed:
# Check video-runtime client generate signature
cat -n packages/video-runtime/src/core/client.ts | head -80Repository: llm-ring/lmring
Length of output: 2461
ai-hub's VideoGenerationParams interface is too limited to support image-to-video generation.
The generateVideo function hardcodes type: 'text-to-video' because ai-hub's VideoGenerationParams interface (in packages/ai-hub/src/types/runtime.ts) has a flat structure with prompt, width, height, and duration as top-level fields. However, the underlying video-runtime supports both text-to-video and image-to-video inputs via a union type VideoGenerationInput, which requires either a prompt field (for text-to-video) or an image field (for image-to-video).
To support both input types, ai-hub's VideoGenerationParams interface should be updated to include an input field matching video-runtime's structure, rather than maintaining a flat property layout.
🤖 Prompt for AI Agents
In `@packages/ai-hub/src/core/video/index.ts` around lines 130 - 141, The
VideoGenerationParams interface is too flat and causes generateVideo (in
packages/ai-hub/src/core/video/index.ts) to hardcode type: 'text-to-video';
update the params type (in packages/ai-hub/src/types/runtime.ts) to include an
input field that matches the runtime's VideoGenerationInput union (i.e., accept
{ type: 'text-to-video', prompt: string } | { type: 'image-to-video', image:
string | Buffer, ... }), then change generateVideo to forward params.input (and
params.input.type) into the client.generate call instead of using a hardcoded
type and top-level prompt/width/height/duration fields so both text-to-video and
image-to-video are supported.
| createProvider(provider: VideoRuntimeProvider, config: VideoRuntimeConfig): VideoProvider { | ||
| // Generate cache key from provider and baseURL | ||
| const cacheKey = `${provider}:${config.baseURL ?? 'default'}`; | ||
|
|
||
| // Return cached instance if available | ||
| const cached = this.cache.get(cacheKey); | ||
| if (cached) { | ||
| return cached; | ||
| } | ||
|
|
||
| // Create new instance | ||
| const instance = this.instantiateProvider(provider, config); | ||
| this.cache.set(cacheKey, instance); | ||
|
|
There was a problem hiding this comment.
Cache key ignores credentials/config, risking cross-tenant leakage.
createProvider caches only by provider + baseURL, so calls with different apiKey/headers/timeouts reuse the first instance and can send requests with the wrong credentials/config. Include a stable config fingerprint in the key (or disable caching when config differs).
🔧 Suggested fix (include config in cache key)
- const cacheKey = `${provider}:${config.baseURL ?? 'default'}`;
+ const cacheKey = JSON.stringify({
+ provider,
+ baseURL: config.baseURL ?? 'default',
+ apiKey: config.apiKey,
+ headers: config.headers ?? {},
+ timeout: config.timeout,
+ pollInterval: config.pollInterval,
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| createProvider(provider: VideoRuntimeProvider, config: VideoRuntimeConfig): VideoProvider { | |
| // Generate cache key from provider and baseURL | |
| const cacheKey = `${provider}:${config.baseURL ?? 'default'}`; | |
| // Return cached instance if available | |
| const cached = this.cache.get(cacheKey); | |
| if (cached) { | |
| return cached; | |
| } | |
| // Create new instance | |
| const instance = this.instantiateProvider(provider, config); | |
| this.cache.set(cacheKey, instance); | |
| createProvider(provider: VideoRuntimeProvider, config: VideoRuntimeConfig): VideoProvider { | |
| // Generate cache key from provider and baseURL | |
| const cacheKey = JSON.stringify({ | |
| provider, | |
| baseURL: config.baseURL ?? 'default', | |
| apiKey: config.apiKey, | |
| headers: config.headers ?? {}, | |
| timeout: config.timeout, | |
| pollInterval: config.pollInterval, | |
| }); | |
| // Return cached instance if available | |
| const cached = this.cache.get(cacheKey); | |
| if (cached) { | |
| return cached; | |
| } | |
| // Create new instance | |
| const instance = this.instantiateProvider(provider, config); | |
| this.cache.set(cacheKey, instance); |
🤖 Prompt for AI Agents
In `@packages/video-runtime/src/core/factory.ts` around lines 33 - 46,
createProvider currently builds cacheKey from provider and baseURL only
(cacheKey) so different configs (apiKey/headers/timeout) reuse the same
instance; update createProvider to compute a stable fingerprint of the relevant
config fields (e.g., API key, headers, timeouts — canonicalized/sorted JSON or a
small hash) and incorporate that fingerprint into cacheKey before
checking/setting this.cache, or alternatively bypass caching when config differs
from the cached instance; update references to cacheKey, instantiateProvider,
and this.cache accordingly so each distinct credential/config produces its own
cached provider.
| resolveModelId(modelId: string): string { | ||
| const stripped = stripProviderPrefix(modelId); | ||
| // Map common names to Google model IDs | ||
| if (stripped === 'veo' || stripped === 'veo-3' || stripped === 'veo-3.0') { | ||
| return 'veo-2.0-generate-001'; | ||
| } | ||
| if (stripped === 'veo-2' || stripped === 'veo-2.0') { | ||
| return 'veo-2.0-generate-001'; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Gemini Veo model IDs Google Generative Language API 2024 2025
💡 Result:
Veo model IDs (Gemini API / Generative Language API)
These are the Veo video-generation model IDs Google documents for API use:
Veo 2 (became available via Google products / APIs in 2025)
veo-2.0-generate-001[1]veo-2.0-generate-preview[1]veo-2.0-generate-exp[1]
Veo 3 (announced available in the Gemini API in 2025)
veo-3.0-generate-001[1]veo-3.0-fast-generate-001[1]veo-3.0-generate-preview(shown in Google’s Gemini API announcement/example) [2]
Veo 3.1 (released to paid preview in the Gemini API in Oct 2025)
veo-3.1-generate-preview[1][3]veo-3.1-fast-generate-preview[1][3]
How to confirm what’s available in your project (programmatically)
Use the Models list endpoint of the Gemini API (Generative Language API) and filter for model names that start with veo-. [4]
2024 note
Veo model IDs as used in the Gemini API / Vertex AI docs above are documented with Veo 2/3/3.1 (i.e., effectively 2025-era availability); if you’re specifically looking for 2024 Generative Language API Veo IDs, they generally weren’t exposed as stable, publicly documented Gemini API model IDs yet (Veo 2 is described as rolling out later, and early access is referenced via Vertex AI). [5]
Sources: Google Cloud Veo model reference / supported models [1], Google AI Developers Forum announcement + code sample for Veo 3 in Gemini API [2], Google Developers Blog (Veo 3.1 / 3.1 Fast in Gemini API) [3], Gemini API (Generative Language API) models.list reference [4], reporting referencing early access timing (context for 2024 vs 2025) [5].
Fix incorrect Veo 3.x model ID mapping.
The code currently maps veo-3 and veo-3.0 to veo-2.0-generate-001, but according to the Gemini API documentation, Veo 3 has distinct model IDs:
veo-3/veo-3.0should map toveo-3.0-generate-001(orveo-3.0-fast-generate-001for the fast variant)
The current mapping will silently route Veo 3 requests to the Veo 2.0 model instead.
🤖 Prompt for AI Agents
In `@packages/video-runtime/src/providers/google.ts` around lines 152 - 160, The
resolveModelId function is incorrectly mapping 'veo-3' and 'veo-3.0' to the Veo
2.0 model; update resolveModelId (and use stripProviderPrefix) so that when
stripped equals 'veo-3' or 'veo-3.0' it returns the correct Veo 3 model ID
(e.g., 'veo-3.0-generate-001' or choose the fast variant
'veo-3.0-fast-generate-001' as appropriate), leaving the existing Veo 2.0
mappings for 'veo-2'/'veo-2.0' unchanged. Ensure the change only affects the
conditional branches in resolveModelId so Veo 3 requests are routed to the
proper Gemini model ID.
| function mapQualityToResolution(quality?: string): string | undefined { | ||
| switch (quality) { | ||
| case 'standard': | ||
| return '720P'; | ||
| case 'high': | ||
| return '1080P'; | ||
| case 'pro': | ||
| case 'master': | ||
| return '4K'; | ||
| default: | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * MiniMax Hailuo video provider. | ||
| */ | ||
| export class MiniMaxVideoProvider extends BaseVideoProvider { | ||
| readonly providerId = 'minimax' as const; | ||
| readonly displayName = 'MiniMax Hailuo'; | ||
| readonly defaultBaseURL = 'https://api.minimax.chat/v1'; | ||
|
|
||
| readonly capabilities: VideoProviderCapabilities = { | ||
| textToVideo: true, | ||
| imageToVideo: true, | ||
| startEndFrames: false, | ||
| audio: true, | ||
| cameraMovement: true, | ||
| maxDurationSeconds: 6, | ||
| aspectRatios: ['16:9', '9:16', '1:1', '4:3', '3:4'], | ||
| qualityTiers: ['standard', 'high'], | ||
| }; |
There was a problem hiding this comment.
mapQualityToResolution supports pro/master but capabilities don't.
qualityTiers excludes pro/master, so validation rejects those values and the mapping is unreachable. Either add them to qualityTiers or drop the extra cases.
✅ Option: expose the mapped tiers
- qualityTiers: ['standard', 'high'],
+ qualityTiers: ['standard', 'high', 'pro', 'master'],📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function mapQualityToResolution(quality?: string): string | undefined { | |
| switch (quality) { | |
| case 'standard': | |
| return '720P'; | |
| case 'high': | |
| return '1080P'; | |
| case 'pro': | |
| case 'master': | |
| return '4K'; | |
| default: | |
| return undefined; | |
| } | |
| } | |
| /** | |
| * MiniMax Hailuo video provider. | |
| */ | |
| export class MiniMaxVideoProvider extends BaseVideoProvider { | |
| readonly providerId = 'minimax' as const; | |
| readonly displayName = 'MiniMax Hailuo'; | |
| readonly defaultBaseURL = 'https://api.minimax.chat/v1'; | |
| readonly capabilities: VideoProviderCapabilities = { | |
| textToVideo: true, | |
| imageToVideo: true, | |
| startEndFrames: false, | |
| audio: true, | |
| cameraMovement: true, | |
| maxDurationSeconds: 6, | |
| aspectRatios: ['16:9', '9:16', '1:1', '4:3', '3:4'], | |
| qualityTiers: ['standard', 'high'], | |
| }; | |
| function mapQualityToResolution(quality?: string): string | undefined { | |
| switch (quality) { | |
| case 'standard': | |
| return '720P'; | |
| case 'high': | |
| return '1080P'; | |
| case 'pro': | |
| case 'master': | |
| return '4K'; | |
| default: | |
| return undefined; | |
| } | |
| } | |
| /** | |
| * MiniMax Hailuo video provider. | |
| */ | |
| export class MiniMaxVideoProvider extends BaseVideoProvider { | |
| readonly providerId = 'minimax' as const; | |
| readonly displayName = 'MiniMax Hailuo'; | |
| readonly defaultBaseURL = 'https://api.minimax.chat/v1'; | |
| readonly capabilities: VideoProviderCapabilities = { | |
| textToVideo: true, | |
| imageToVideo: true, | |
| startEndFrames: false, | |
| audio: true, | |
| cameraMovement: true, | |
| maxDurationSeconds: 6, | |
| aspectRatios: ['16:9', '9:16', '1:1', '4:3', '3:4'], | |
| qualityTiers: ['standard', 'high', 'pro', 'master'], | |
| }; |
🤖 Prompt for AI Agents
In `@packages/video-runtime/src/providers/minimax.ts` around lines 124 - 155,
mapQualityToResolution includes 'pro'/'master' but
MiniMaxVideoProvider.capabilities.qualityTiers does not, causing those branches
to be unreachable; update the capabilities to include 'pro' and 'master' in
qualityTiers (or remove the 'pro'/'master' cases in mapQualityToResolution) so
validation and mapping align, specifically modify
MiniMaxVideoProvider.capabilities.qualityTiers to add 'pro' and 'master' if you
want to expose those higher tiers.
| readonly capabilities: VideoProviderCapabilities = { | ||
| textToVideo: true, | ||
| imageToVideo: true, | ||
| startEndFrames: false, | ||
| audio: false, | ||
| cameraMovement: false, | ||
| maxDurationSeconds: 20, | ||
| aspectRatios: ['16:9', '9:16', '1:1'], | ||
| qualityTiers: ['standard', 'high'], | ||
| }; | ||
|
|
||
| resolveModelId(modelId: string): string { | ||
| const stripped = stripProviderPrefix(modelId); | ||
| // Default to sora-2 if just 'sora' is provided | ||
| if (stripped === 'sora') { | ||
| return 'sora-2'; | ||
| } | ||
| return stripped; | ||
| } | ||
|
|
||
| protected async createTask(params: VideoGenerationParams, signal?: AbortSignal): Promise<string> { | ||
| const model = this.resolveModelId(params.model); | ||
| const input = params.input; | ||
|
|
||
| const body: OpenAIVideoRequest = { | ||
| model, | ||
| prompt: input.prompt, | ||
| size: aspectRatioToSize(params.aspectRatio, params.width, params.height), | ||
| seconds: params.duration, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/video-runtime/src/providers/openai.ts | head -200Repository: llm-ring/lmring
Length of output: 6192
🏁 Script executed:
rg "VideoGenerationParams" --type ts --type tsx -A 10 -B 2 | head -100Repository: llm-ring/lmring
Length of output: 86
🏁 Script executed:
rg "quality" packages/video-runtime/src/providers/ --type ts -B 2 -A 2Repository: llm-ring/lmring
Length of output: 5050
🌐 Web query:
OpenAI Sora video generation API quality parameter request field documentation
💡 Result:
The Sora video generation endpoint (POST https://api.openai.com/v1/videos) does not document a quality parameter in the request body. The documented request fields are prompt (required), plus optional input_reference, model (sora-2 or sora-2-pro), seconds (4/8/12), and size (e.g., 1280x720, 720x1280, etc.). [1]
“Quality” is effectively controlled by model selection (use sora-2-pro for higher-quality output) and your chosen size/seconds, rather than a separate quality request field. [2][3]
A quality field may appear in responses (example shows "quality": "standard"), but it is not shown as a configurable request parameter in the API reference. [1]
Sources
[1] OpenAI API Reference — Videos: Create video (/v1/videos)
[2] OpenAI API Guide — Video generation with Sora (model differences)
[3] Sora 2 Prompting Guide — API parameters (quality governed by API parameters; configured via fields like model/size/seconds)
qualityTiers exposed but params.quality is ignored — either remove capability or map to model variants.
OpenAI Sora API doesn't accept a quality parameter in requests. Quality is controlled via model selection (sora-2 vs sora-2-pro). The current implementation advertises quality tiers but never uses params.quality, misleading users. Either remove qualityTiers from capabilities, or implement quality mapping: 'high' → 'sora-2-pro' and 'standard' → 'sora-2' in the resolveModelId() method.
🤖 Prompt for AI Agents
In `@packages/video-runtime/src/providers/openai.ts` around lines 116 - 145, The
VideoProvider currently advertises qualityTiers but ignores params.quality;
update resolveModelId (or createTask) to map quality into the model name: if
params.quality === 'high' select 'sora-2-pro', if 'standard' select 'sora-2'
(while still allowing explicit provider-prefixed modelIds), or alternatively
remove qualityTiers from the capabilities object; specifically modify
resolveModelId(modelId: string) to accept an optional quality hint (or read
params.quality before calling resolveModelId in createTask) and return
'sora-2-pro' for high and 'sora-2' for standard, keeping existing
stripProviderPrefix behavior for explicit model names.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if the logo files exist and inspect their content
echo "=== Checking for SVG files in apps/web/public/ ==="
fd -t f -e svg "athena" apps/web/public/ 2>/dev/null || echo "No athena SVG files found"
echo -e "\n=== Checking all SVG files in apps/web/public/ ==="
fd -t f -e svg . apps/web/public/ 2>/dev/null || echo "No SVG files found"
echo -e "\n=== Checking if apps/web/public directory exists ==="
ls -la apps/web/public/ 2>/dev/null || echo "Directory does not exist or is not accessible"
echo -e "\n=== Broader search for athena files ==="
fd -t f "athena" . 2>/dev/null | head -20Repository: llm-ring/lmring
Length of output: 1050
🏁 Script executed:
#!/bin/bash
# Inspect the SVG content to determine actual colors and styling
echo "=== Content of athena-white.svg ==="
cat apps/web/public/athena-white.svg
echo -e "\n\n=== Content of athena-black.svg ==="
cat apps/web/public/athena-black.svgRepository: llm-ring/lmring
Length of output: 16511
Use the black logo variant for README visibility on light backgrounds.
The athena-white.svg logo has a white fill (fill:#fff``) that will be invisible or difficult to read on GitHub's default light theme. GitHub's README displays with a light background by default, making white logos invisible to users not using dark mode.
Replace with athena-black.svg, which already exists in the repository and will be visible on both light and dark backgrounds:
Suggested fix
- <h1><img src="apps/web/public/athena-white.svg" alt="LMRing Logo" height="32" style="vertical-align: middle;" /> <a href="https://www.lmring.com/">LMRING</a></h1>
+ <h1><img src="apps/web/public/athena-black.svg" alt="LMRing Logo" height="32" style="vertical-align: middle;" /> <a href="https://www.lmring.com/">LMRING</a></h1>🤖 Prompt for AI Agents
In `@README.md` at line 2, In README.md replace the white-logo reference by
updating the image src "apps/web/public/athena-white.svg" to
"apps/web/public/athena-black.svg" (keep the existing <img> attributes and alt
text) so the logo is visible on light backgrounds; verify the <h1> line still
renders correctly and commit the updated README.

PR Type
Related Issue
Closes #
Description
Add end-to-end video generation capability to LMRing, including a new
video-runtimepackage with multi-provider support and streaming workflow integration in the web app.Changes
packages/video-runtime— unified video generation runtime supporting Google (Veo), OpenAI (Sora), Kling, MiniMax, Seedance, and Vidu providers/api/workflow/video-stream) with SSE-based progress updatesVideoAttachmentandVideoPlayercomponentsai-hubwith video generation core modulemodel-typeutility inmodel-depotfor text/video model classificationx-languageheader skip for static resourcesAffected Packages
apps/webpackages/ai-hubpackages/authpackages/databasepackages/envpackages/i18npackages/model-depotpackages/storagepackages/uiTesting
Checklist
pnpm lint:fixpassespnpm check:typespassespnpm testpassespnpm buildpassesScreenshots (Optional)
Summary by CodeRabbit
Release Notes
New Features
Changes