diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index b819ad14347..079a3b0dfef 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -111,8 +111,8 @@ jobs: echo "📍 Submodule commit: $CURRENT_COMMIT" # Check if this version exists on npm - if npm view @answerai/aai-embed@$EMBED_VERSION version 2>/dev/null; then - echo "✅ @answerai/aai-embed@$EMBED_VERSION is published on npm" + if npm view aai-embed@$EMBED_VERSION version 2>/dev/null; then + echo "✅ aai-embed@$EMBED_VERSION is published on npm" # Optional: Verify git tag matches commit for extra safety EXPECTED_TAG="v${EMBED_VERSION}" @@ -132,13 +132,13 @@ jobs: echo "is_published=true" >> $GITHUB_OUTPUT echo "embed_version=$EMBED_VERSION" >> $GITHUB_OUTPUT else - echo "❌ @answerai/aai-embed@$EMBED_VERSION NOT found on npm" + echo "❌ aai-embed@$EMBED_VERSION NOT found on npm" echo "" echo "🛑 Cannot publish aai-embed-react because aai-embed@$EMBED_VERSION hasn't been published yet." echo "" echo "Next steps:" echo "1. Ensure aai-embed workflow ran successfully in the chat-embed repository" - echo "2. Verify the package was published: npm view @answerai/aai-embed@$EMBED_VERSION" + echo "2. Verify the package was published: npm view aai-embed@$EMBED_VERSION" echo "3. If not published, trigger the workflow manually in the chat-embed repo" echo "is_published=false" >> $GITHUB_OUTPUT @@ -162,12 +162,19 @@ jobs: packages: write id-token: write steps: + - name: Generate GitHub App Token + id: generate-token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.STAGING_RESET_APP_ID }} + private_key: ${{ secrets.STAGING_RESET_APP_PRIVATE_KEY }} + - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -253,8 +260,7 @@ jobs: - name: Update package metadata working-directory: ./packages/embed-react run: | - # Update package name to scoped name - node -e "const pkg = require('./package.json'); pkg.name = '@answerai/aai-embed-react'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" + # Keep package name as-is (aai-embed-react) # Add publishConfig node -e "const pkg = require('./package.json'); pkg.publishConfig = { access: 'public' }; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));" @@ -309,6 +315,6 @@ jobs: - name: Summary run: | - echo "✅ Published @answerai/aai-embed-react@${{ steps.version.outputs.new_version }} to npm" + echo "✅ Published aai-embed-react@${{ steps.version.outputs.new_version }} to npm" echo "✅ Peer dependency: aai-embed@^${{ steps.embed_version.outputs.embed_version }}" echo "✅ Created tag aai-embed-react-v${{ steps.version.outputs.new_version }}" diff --git a/packages-answers/ui/src/VideoCreator/VideoCreator.Client.tsx b/packages-answers/ui/src/VideoCreator/VideoCreator.Client.tsx index 0d72e9bc6eb..4bbce571dcf 100644 --- a/packages-answers/ui/src/VideoCreator/VideoCreator.Client.tsx +++ b/packages-answers/ui/src/VideoCreator/VideoCreator.Client.tsx @@ -178,7 +178,18 @@ const getProviderForModel = (model: string): Provider => { return 'openai' } -const durationOptions = [4, 6, 8, 10, 12, 15] +/** + * Provider-specific duration constraints based on provider API documentation. + * Frontend uses these discrete sets so users cannot pick unsupported durations. + */ +const getProviderDurationOptions = (provider: Provider): number[] => { + if (provider === 'openai') { + // OpenAI Sora models support only 4, 8, and 12 second videos. + return [4, 8, 12] + } + // Google Veo models support discrete duration values. + return [4, 6, 8] +} const formatDateTime = (value?: string) => { if (!value) return 'Unknown date' @@ -236,6 +247,7 @@ const VideoCreator = () => { const [dialogEmotion, setDialogEmotion] = useState('calm') const provider = useMemo(() => getProviderForModel(model), [model]) + const durationOptions = useMemo(() => getProviderDurationOptions(provider), [provider]) const jobPollers = useRef>(new Map()) useEffect(() => { @@ -333,6 +345,8 @@ const VideoCreator = () => { } useEffect(() => { + const validDurations = getProviderDurationOptions(provider) + if (provider === 'google') { // Veo: 1080p only supports 16:9, 720p supports both 16:9 and 9:16 setAspectRatio((current) => { @@ -344,10 +358,16 @@ const VideoCreator = () => { return current }) setSize((current) => (current === '1280x720' || current === '1920x1080' ? current : '1280x720')) - setSeconds((current) => (durationOptions.includes(current) ? current : 8)) + setSeconds((current) => (validDurations.includes(current) ? current : 8)) } else { + // OpenAI: validate against strict constraints [4, 8, 12] setSize((current) => (current === '1280x720' || current === '1920x1080' ? current : '1280x720')) - setSeconds((current) => (durationOptions.includes(current) ? current : 8)) + setSeconds((current) => { + if (validDurations.includes(current)) return current + // Find closest valid duration for OpenAI (4, 8, or 12) + const closest = validDurations.reduce((prev, curr) => (Math.abs(curr - current) < Math.abs(prev - current) ? curr : prev)) + return closest + }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [provider]) @@ -1087,9 +1107,12 @@ const VideoCreator = () => { setSeconds(newValue as number)} - min={4} - max={15} - step={1} + min={Math.min(...durationOptions)} + max={Math.max(...durationOptions)} + step={null} + marks={durationOptions.map((duration) => ({ + value: duration + }))} disabled={loading} size='small' sx={{ flexGrow: 1 }} diff --git a/packages/embed b/packages/embed index 3e869741ab7..b913fd314c5 160000 --- a/packages/embed +++ b/packages/embed @@ -1 +1 @@ -Subproject commit 3e869741ab7acfdc2cf643d877f9ae071f4884fa +Subproject commit b913fd314c577578ff4e33bb29b51ad5711e7b8e diff --git a/packages/server/src/aai-utils/billing/config.ts b/packages/server/src/aai-utils/billing/config.ts index be1c8fc9d33..1cb58070c6c 100644 --- a/packages/server/src/aai-utils/billing/config.ts +++ b/packages/server/src/aai-utils/billing/config.ts @@ -42,9 +42,13 @@ export const BILLING_CONFIG = { // Sync configuration SYNC: { - LOOKBACK_DAYS: parseInt(process.env.BILLING_SYNC_LOOKBACK_DAYS || '90'), - PAGE_BATCH_SIZE: 15, // Number of pages to fetch in parallel - RATE_LIMIT_DELAY_MS: 1000 // Delay between page batches + LOOKBACK_DAYS: parseInt(process.env.BILLING_SYNC_LOOKBACK_DAYS || '7'), // Reduced from 90 to 7 days + PAGE_BATCH_SIZE: parseInt(process.env.BILLING_SYNC_PAGE_BATCH_SIZE || '3'), // Reduced from 15 to 3 pages + RATE_LIMIT_DELAY_MS: parseInt(process.env.BILLING_SYNC_RATE_LIMIT_MS || '2000'), // Increased from 1000 to 2000ms + TRACE_BATCH_SIZE: parseInt(process.env.BILLING_SYNC_TRACE_BATCH_SIZE || '5'), // Reduced from 15 to 5 traces + MAX_RETRIES: 3, + RETRY_DELAY_MS: 1000, + EXPONENTIAL_BACKOFF: true }, // Resource configuration diff --git a/packages/server/src/aai-utils/billing/langfuse/LangfuseProvider.ts b/packages/server/src/aai-utils/billing/langfuse/LangfuseProvider.ts index 10a5280308a..5ed913d5a4b 100644 --- a/packages/server/src/aai-utils/billing/langfuse/LangfuseProvider.ts +++ b/packages/server/src/aai-utils/billing/langfuse/LangfuseProvider.ts @@ -78,6 +78,7 @@ export class LangfuseProvider { page?: number userId?: string filter?: any[] + fields?: string }): Promise { const queryParams: any = { ...params } @@ -218,12 +219,14 @@ export class LangfuseProvider { }) // Fetch and process first page + // Use minimal fields for initial filtering - exclude observations & scores for performance const initialResponse = await this.fetchTraces({ fromTimestamp: fromTimestamp.toISOString(), toTimestamp: toTimestamp.toISOString(), limit: 100, page: 1, - filter: LangfuseProvider.UNPROCESSED_FILTER + filter: LangfuseProvider.UNPROCESSED_FILTER, + fields: 'core,metrics,io' // Exclude observations & scores - reduces payload by 80-90% }) // console.log('initialResponse', initialResponse) const totalPages = initialResponse.meta.totalPages @@ -312,7 +315,8 @@ export class LangfuseProvider { toTimestamp: toTimestamp.toISOString(), limit: 100, page, - filter: LangfuseProvider.UNPROCESSED_FILTER + filter: LangfuseProvider.UNPROCESSED_FILTER, + fields: 'core,metrics,io' // Exclude observations & scores - reduces payload by 80-90% }) ) } @@ -547,13 +551,15 @@ export class LangfuseProvider { startDate.setDate(startDate.getDate() - 30) // Fetch traces from Langfuse API with pagination + // Use minimal fields for usage events display - exclude observations to reduce load const langfuseResponse = await this.fetchTraces({ fromTimestamp: startDate.toISOString(), toTimestamp: endDate.toISOString(), limit, page, userId, - filter: LangfuseProvider.UNPROCESSED_FILTER + filter: LangfuseProvider.UNPROCESSED_FILTER, + fields: 'core,metrics,io' // Exclude observations & scores - faster response // Note: We can't directly filter by customerId in the API call // We'll filter the results after fetching }) diff --git a/packages/server/src/services/video-generator/index.ts b/packages/server/src/services/video-generator/index.ts index 4150c298264..cb8532bbbd4 100644 --- a/packages/server/src/services/video-generator/index.ts +++ b/packages/server/src/services/video-generator/index.ts @@ -14,6 +14,9 @@ import { Blob } from 'node:buffer' const STORAGE_FOLDER = 'generated-videos' const OPENAI_MODELS = new Set(['sora-2', 'sora-2-pro']) const GOOGLE_MODELS = new Set(['veo-3.0-generate-001', 'veo-3.0-fast-generate-001']) +// Duration constraints based on provider API specifications +const OPENAI_VALID_DURATIONS = [4, 8, 12] // OpenAI Sora: discrete values only +const GOOGLE_VALID_DURATIONS = [4, 6, 8] // Google Veo: supported discrete values const POLL_INTERVAL_MS = 5000 const MAX_POLL_ATTEMPTS = 120 // ~10 minutes const JOB_RETENTION_MS = 24 * 60 * 60 * 1000 // keep completed jobs for 24 hours @@ -148,6 +151,33 @@ const mapOpenAIStatus = (status?: string): JobStatus => { } } +/** + * Validates and normalizes video duration based on provider constraints. + * Auto-corrects invalid values to the nearest supported duration instead of failing. + * + * @param seconds - Requested duration in seconds + * @param model - Video generation model name + * @returns Duration guaranteed to be accepted by the downstream provider + */ +const validateAndNormalizeDuration = (seconds: number | undefined, model: string): number => { + if (!seconds) return 8 // Works for both providers + + const isOpenAI = OPENAI_MODELS.has(model) + const validDurations = isOpenAI ? OPENAI_VALID_DURATIONS : GOOGLE_VALID_DURATIONS + + if (validDurations.includes(seconds)) { + return seconds + } + + const normalized = validDurations.reduce((previous, current) => + Math.abs(current - seconds) < Math.abs(previous - seconds) ? current : previous + ) + + console.warn(`[Video Generator] Adjusted duration from ${seconds}s to ${normalized}s for model ${model} (provider constraint)`) + + return normalized +} + const getStorageType = (): string => { return process.env.STORAGE_TYPE ? process.env.STORAGE_TYPE : 'local' } @@ -677,6 +707,9 @@ const startOpenAIJob = async (request: VideoGenerationRequest): Promise processOpenAIJob(jobId, { ...request, prompt }, creationResponse)) + // Pass normalized seconds for consistent telemetry/logging downstream + setImmediate(() => processOpenAIJob(jobId, { ...request, prompt, seconds: validatedSeconds }, creationResponse)) return job } @@ -1102,6 +1136,7 @@ const startGoogleJob = async (request: VideoGenerationRequest): Promise processGoogleJob(jobId, request, operation)) + // Pass normalized seconds for consistent telemetry/logging downstream + setImmediate(() => processGoogleJob(jobId, { ...request, seconds: validatedSeconds }, operation)) return job }