v1.1.0 feat: add video generation support with multi-provider runtime by lifefloating · Pull Request #227 · llm-ring/lmring · GitHub
Skip to content

v1.1.0 feat: add video generation support with multi-provider runtime - #227

Merged
lifefloating merged 38 commits into
mainfrom
develop
Feb 3, 2026
Merged

lifefloating merged 38 commits into
mainfrom
develop

Conversation

@lifefloating

@lifefloating lifefloating commented Feb 3, 2026 •

Copy link
Copy Markdown
Member

PR Type

  • ✨ Feature
  • 🐛 Bug Fix
  • ♻️ Refactor
  • 📝 Documentation
  • 🔧 CI/CD
  • ⬆️ Dependencies
  • 🎨 Style
  • 🧪 Test
  • 🗑️ Chore

Related Issue

Closes #

Description

Add end-to-end video generation capability to LMRing, including a new video-runtime package with multi-provider support and streaming workflow integration in the web app.

Changes

  • Add packages/video-runtime — unified video generation runtime supporting Google (Veo), OpenAI (Sora), Kling, MiniMax, Seedance, and Vidu providers
  • Add video streaming workflow API (/api/workflow/video-stream) with SSE-based progress updates
  • Integrate video display into chat message flow with VideoAttachment and VideoPlayer components
  • Add video input mode and model cards for video models (Veo 3.1, Together AI video models, MiniMax, OpenAI)
  • Extend ai-hub with video generation core module
  • Add model-type utility in model-depot for text/video model classification
  • Update sidebar styling and temporarily disable voting on homepage
  • Renew logo assets (favicon, apple-touch-icon, SVG variants)
  • Add x-language header skip for static resources
  • Add i18n keys for video-related UI (en, fr, zh)

Affected Packages

  • apps/web
  • packages/ai-hub
  • packages/auth
  • packages/database
  • packages/env
  • packages/i18n
  • packages/model-depot
  • packages/storage
  • packages/ui

Testing

  • Unit tests
  • E2E tests
  • Manual testing

Checklist

  • pnpm lint:fix passes
  • pnpm check:types passes
  • pnpm test passes
  • pnpm build passes
  • Commit message follows Conventional Commits
  • Documentation updated (if needed)

Screenshots (Optional)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added video generation capabilities with support for multiple providers (OpenAI, Google, MiniMax, and others)
    • Introduced video player component and video display in chat
    • Added video model selection in provider settings
    • Updated landing page with new branding and tagline
    • Enhanced localization with new translation keys
  • Changes

    • Disabled voting functionality in the Arena
    • Updated dependencies across packages to latest versions
    • Version bump to 1.1.0

renovate Bot and others added 30 commits January 28, 2026 13:13
fix(deps): update dependency react-i18next to ^16.5.4
fix(deps): update react ecosystem to ^19.2.4
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
@vercel

vercel Bot commented Feb 3, 2026

Copy link
Copy Markdown

@lifefloating lifefloating self-assigned this Feb 3, 2026
@coderabbitai

coderabbitai Bot commented Feb 3, 2026 •

Copy link
Copy Markdown

Walkthrough

This PR introduces comprehensive video generation support across the application stack. It adds a new packages/video-runtime package with implementations for multiple video providers (OpenAI Sora, Google Veo, MiniMax, Kling, Seedance, Vidu, and OpenAI-compatible), integrates video workflows into the Arena chat interface, adds UI components for video display and generation, creates backend video streaming endpoints, extends the type system and model metadata, and bumps all package versions to 1.1.0.

Changes

Cohort / File(s) Summary
Video Runtime Package
packages/video-runtime/*
New complete video generation framework with core client/router/factory, seven provider implementations (google, openai, minimax, kling, seedance, vidu, openai-compatible), comprehensive type definitions, error handling, validation, and HTTP utilities totaling ~2000+ lines.
Package Version Bumps
package.json, packages/*/package.json
Version bumped from 1.0.6 to 1.1.0 across root and all subpackages; updated dependencies (@lobehub/ui, framer-motion, react, react-i18n, zustand, better-auth) and added video-runtime dependency in ai-hub and apps/web.
Model Depot Video Support
packages/model-depot/src/models/{google,openai,minimax,togetherai}.ts, packages/model-depot/src/types/model.ts, packages/model-depot/src/utils/model-type.ts
Extended model system with VideoModelCard interface, added video model definitions for Google Veo, OpenAI Sora, MiniMax Hailuo, Vidu, and Together AI; added utilities (isVideoModel, getRuntimeModelId, getRuntimeProvider) and new exports map entry.
AI Hub Video Module
packages/ai-hub/src/core/video/index.ts, packages/ai-hub/src/types/runtime.ts, packages/ai-hub/src/index.ts
Introduced VideoGenerationConfig, generateVideo async generator, and VideoStreamEvent types as backward-compatible wrappers to video-runtime package; added public module exports.
Arena Video Workflows
apps/web/src/app/(auth)/arena/[[...conversationId]]/page.tsx, apps/web/src/hooks/use-workflow-execution.ts, apps/web/src/stores/workflow-store.tsx
Integrated video workflow execution with executeVideoWorkflowStream and startAllSyncedVideoWorkflows; extended onSaveModelResponse signature with optional explicitAttachments parameter; added video generation state tracking and completion handling in workflow store.
Video UI Components
apps/web/src/components/arena/video-player.tsx, apps/web/src/components/arena/video-attachment.tsx, apps/web/src/components/arena/video-response.tsx, apps/web/src/components/arena/chat/message.tsx, apps/web/src/components/arena/chat/chat-list.tsx
Added VideoPlayer component with plyr-react, VideoAttachmentDisplay and VideoGeneratingIndicator for attachment display, VideoResponse container for video state, integrated isVideoGenerating prop through chat message pipeline.
Arena Input & UI
apps/web/src/components/arena/prompt-input-features.tsx, apps/web/src/components/arena/arena-hero-section.tsx, apps/web/src/components/arena/model-card.tsx, apps/web/src/components/arena/vote-bar.tsx
Added video generation mode toggle with SquarePlay icon; disabled voting feature (return null, no-op handlers); randomized hero title selection; removed vote-related imports and animations.
API Video Streaming
apps/web/src/app/api/workflow/video-stream/route.ts, apps/web/src/libs/video-workflow-api.ts
Implemented POST /api/workflow/video-stream SSE endpoint for streaming video generation progress with heartbeats, result handling, storage upload, and error emission; added buildVideoStreamRequest and streamVideoWorkflow client-side streaming utilities.
Attachment & Storage
apps/web/src/app/api/conversations/[id]/full/route.ts, apps/web/src/app/api/model-responses/route.ts, apps/web/src/app/api/settings/api-keys/check/route.ts, packages/database/src/schema.ts
Added attachmentsWithUrls helper to convert storage keys to signed URLs; extended attachments with url field; short-circuited video model connectivity checks with skip reason; added optional url field to ResponseAttachment interface.
Type System & Configuration
apps/web/src/types/input-mode.ts, apps/web/src/types/workflow.ts
Added 'videoGenerate' input mode with videoOutput ability mapping; extended WorkflowMessage with videoAttachment field, PendingResponse with isVideoGenerating flag, WorkflowStreamEventType with heartbeat/video variants.
Landing Page & Layout
apps/web/src/app/(landing)/page.tsx, apps/web/src/app/(landing)/page.test.tsx, apps/web/src/components/landing/animated-hero.tsx, apps/web/src/components/landing/animated-hero.test.tsx, apps/web/src/app/layout.tsx, README.md
Updated landing page with gradient tagline/subtitle, logo prop to AnimatedHero, added athena logo images; updated test expectations; added smooth scroll behavior to html tag; embedded logo in README header.
Sidebar & Branding
apps/web/src/components/sidebar.tsx, apps/web/src/components/sidebar.test.tsx
Replaced SparklesIcon with Image components for light/dark logo variants; updated rendered title from 'LMRing' to 'lmring'.
Localization
apps/web/src/locales/{en,fr,zh}.json
Added translation keys: Index.tagline, Index.subtitle, Arena.video_generation, Arena.hero_title_alt across all three language files.
Configuration & Templates
.github/pull_request_template.md
Added packages/video-runtime to Affected Packages list in PR template.
Service Worker & Utilities
apps/web/public/language-sw.js, apps/web/src/hooks/use-translations.ts
Added static resource pre-check in fetch handler to skip language augmentation for assets and /_next/ paths; removed biome-ignore comment from translate wrapper.
Test Updates
apps/web/src/app/(auth)/settings/_components/provider/ProviderDetail.test.tsx, apps/web/src/components/arena/model-card.test.tsx, apps/web/src/components/arena/vote-bar.test.tsx, packages/video-runtime/src/index.test.ts, apps/web/src/types/input-mode.test.ts
Added VideoIcon mock; updated voting tests to validate disabled behavior; added videoGenerate to input mode mapping test; new video-runtime API export validation tests.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • PR #29 — Both modify ProviderDetail.tsx and provider metadata system, directly related through settings UI changes
  • PR #157 — Both extend attachment/storage surface with signed URLs and add attachment fields in API routes
  • PR #214 — Both modify apps/web/src/app/(auth)/arena/[[...conversationId]]/page.tsx with workflow state and store selector changes

Suggested labels

major-update, enhancement

Suggested reviewers

  • daguanren21

Poem

🐰 A video-generating hare hops with glee,
Through streams and providers, from OpenAI to Vidu so free,
With workflows that flutter and attachments that flow,
The Arena now gleams with each generated show! 🎬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: adding video generation support with a multi-provider runtime, which aligns with the primary objectives and changes in the PR.
Description check ✅ Passed The description covers PR type, affected packages, key changes, and testing status. However, the Conventional Commits checklist item is unchecked, and documentation updates are not confirmed as complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lifefloating
lifefloating merged commit a6e1e49 into main Feb 3, 2026
3 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Add 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-label and 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 | 🟡 Minor

Include 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 /public or /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 | 🟡 Minor

Align the settings-page collapse assertion with the new lowercase branding.

The title now renders as lmring, but the settings-page test still checks for LMRing, 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 | 🟡 Minor

Check 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 | 🟡 Minor

Add 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 | 🟡 Minor

Add 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 | 🟡 Minor

Localize 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 | 🟡 Minor

Use provider mimeType for upload content type and filename.
Hardcoding video/mp4 and the .mp4 extension 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 | 🟡 Minor

Add 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 | 🟡 Minor

Potential state inconsistency when stopping video generation.

When setVideoGenerating(id, false) is called, pendingResponse is cleared but status is preserved via workflow.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 completePendingResponse sets status: '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 | 🟡 Minor

Reject non-positive durations to avoid invalid requests.

params.duration is 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 | 🟡 Minor

Avoid JSON-stringifying non-JSON request bodies.

httpRequest always JSON.stringifys any provided body, which breaks if callers pass a string, FormData, or other BodyInit types. This also forces Content-Type: application/json even 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 | 🟡 Minor

Handle synchronous completion without dropping the video result.

If Google ever returns a synchronous done response, you return "sync-completed" but never preserve the actual video payload—pollTaskStatus then yields completed with result: 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 a progress event 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, and error variants. Since video generation can be a long-running operation, a progress event 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 extracting attachmentsWithUrls to 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/attachments or 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 for RootLayout.

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 explicit disabled={false} to clarify test intent.

The test verifies that VoteBar renders nothing, but doesn't explicitly pass disabled={false}. Since the implementation unconditionally returns null (regardless of the disabled prop), 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 for VoteBar component.

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:

  1. Keeping the original implementation in a separate git branch for reference
  2. Using a feature flag/environment variable to toggle voting at runtime
  3. 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 for useCardVoting hook.

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.

effectiveVoteState is computed but only used to look up empty strings from voteStateBorderStyles. 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 cardClasses to 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.Element or JSX.Element as 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 done is true, there may be remaining bytes in the decoder's internal buffer that need to be flushed. Additionally, any remaining content in buffer after 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 using interface for 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 false for 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 to VideoGenerationParams to avoid unsafe casts.
Casting in parseParams can 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 shared TextToVideoInput interface 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: Prefer vi.spyOn for 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 for IIndexProps.

🔧 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 VideoAttachmentDisplay and VideoGeneratingIndicator lack explicit return types. Per TypeScript best practices and project guidelines, add : JSX.Element to both functions.

packages/model-depot/src/models/togetherai.ts (1)

323-342: Consider adding maxDurationSeconds for consistency.

The Wan-AI models (Wan2.2-I2V-A14B and Wan2.2-T2V-A14B) are missing maxDurationSeconds, resolutions, and fps properties 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 resolveModelId method has redundant branches. The final return stripped at line 153 handles the startsWith('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 in isOfficialGoogleEndpoint.

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 to FullModelCard for completeness.

FullModelCard serves as a unified type but lacks video-specific fields like maxDurationSeconds and fps that exist in VideoModelCard. This could lead to data loss when converting from VideoModelCard to FullModelCard.

♻️ 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.

isSupported and getSupportedProviders each 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 actual data shapes.

extractVideoUrl handles both array and object forms, but OpenAICompatibleVideoResponse.data only 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: Align aspectRatioToSize mappings with declared capabilities.

aspectRatioToSize includes 21:9, but capabilities.aspectRatios doesn't, so validation blocks it. Either add 21:9 to 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.

✍️ 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> => {
     ...
   }, [...]);
As per coding guidelines, Use explicit return types for functions in TypeScript.
apps/web/src/app/(auth)/arena/[[...conversationId]]/page.tsx (1)

1046-1169: Add an explicit return type to handleSubmit.

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.

Comment on lines +30 to +34
const heroTitleKey = useMemo(() => {
return (
HERO_TITLE_KEYS[Math.floor(Math.random() * HERO_TITLE_KEYS.length)] ?? HERO_TITLE_KEYS[0]
);
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "arena-hero-section.tsx" -type f

Repository: llm-ring/lmring

Length of output: 113


🏁 Script executed:

cat -n apps/web/src/components/arena/arena-hero-section.tsx

Repository: llm-ring/lmring

Length of output: 2104


🏁 Script executed:

head -50 apps/web/src/components/arena/arena-hero-section.tsx | cat -n

Repository: 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.

Comment on lines +11 to +35
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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/components/arena/video-response.tsx | head -40

Repository: 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.

Suggested change
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.

Comment on lines +439 to +537
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.tsx

Repository: llm-ring/lmring

Length of output: 2397


🏁 Script executed:

rg -n "setWorkflowStatus" apps/web/src/hooks/use-workflow-execution.ts -A 2 | head -30

Repository: llm-ring/lmring

Length of output: 1124


🏁 Script executed:

sed -n '439,537p' apps/web/src/hooks/use-workflow-execution.ts

Repository: 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 30

Repository: llm-ring/lmring

Length of output: 2647


🏁 Script executed:

rg -n "pendingResponse" apps/web/src/stores/workflow-store.tsx | head -50

Repository: 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:

  1. 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 clear pendingResponse for failed status (workflow-store.tsx line 378 only clears on 'cancelled'). Add setVideoGenerating(id, false) in a finally block.

  2. Conversation creation heartbeat-dependent: Conversation setup only triggers on heartbeat events (line 466). If the stream emits video or complete first, 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.

Comment on lines +130 to +141
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 2

Repository: 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 tsx

Repository: 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 -20

Repository: 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 2

Repository: 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 -100

Repository: 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 -80

Repository: 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.

Comment on lines +33 to +46
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +152 to +160
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';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.0 should map to veo-3.0-generate-001 (or veo-3.0-fast-generate-001 for 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.

Comment on lines +124 to +155
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'],
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +116 to +145
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,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/video-runtime/src/providers/openai.ts | head -200

Repository: llm-ring/lmring

Length of output: 6192


🏁 Script executed:

rg "VideoGenerationParams" --type ts --type tsx -A 10 -B 2 | head -100

Repository: llm-ring/lmring

Length of output: 86


🏁 Script executed:

rg "quality" packages/video-runtime/src/providers/ --type ts -B 2 -A 2

Repository: 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.

Comment thread README.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: 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.svg

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants