From cec42a7f201f3bed84f0c983bfb193caacc78487 Mon Sep 17 00:00:00 2001 From: analogpvt Date: Thu, 4 Jun 2026 08:13:54 +0000 Subject: [PATCH 1/3] feat(reve): add Reve image generation provider Add Reve as a new image generation provider with the reve-create model. - Add provider definition with env config (LLM_REVE_API_KEY) - Add reve-create model ($0.024/image, native 4K, text-to-image) - Add provider endpoint routing (https://api.reve.com/v1/image/create) - Add request body transformation (prompt, version, aspect_ratio) - Add response parsing for Reve's {image: base64} format - Add OpenAI-format response construction for non-choices responses - Update .env.example with LLM_REVE_API_KEY placeholder Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 2 + .../src/chat/tools/parse-provider-response.ts | 18 ++++++ .../tools/transform-response-to-openai.ts | 57 +++++++++++++++++++ packages/actions/src/get-provider-endpoint.ts | 8 +++ packages/actions/src/prepare-request-body.ts | 44 ++++++++++++++ packages/models/src/models.ts | 2 + packages/models/src/models/reve.ts | 30 ++++++++++ packages/models/src/providers.ts | 20 +++++++ 8 files changed, 181 insertions(+) create mode 100644 packages/models/src/models/reve.ts diff --git a/.env.example b/.env.example index 113d044768..dc9d36d73a 100644 --- a/.env.example +++ b/.env.example @@ -221,6 +221,8 @@ LLM_AZURE_AI_FOUNDRY_RESOURCE=your_azure_ai_foundry_resource_here # CanopyWave LLM_CANOPY_WAVE_API_KEY=your_canopywave_key_here +# Reve +LLM_REVE_API_KEY=your_reve_api_key_here # ============================================================================= # ANALYTICS (OPTIONAL) diff --git a/apps/gateway/src/chat/tools/parse-provider-response.ts b/apps/gateway/src/chat/tools/parse-provider-response.ts index 0c4569d905..d3b9f92b43 100644 --- a/apps/gateway/src/chat/tools/parse-provider-response.ts +++ b/apps/gateway/src/chat/tools/parse-provider-response.ts @@ -690,6 +690,24 @@ export function parseProviderResponse( } break; } + // Check if this is a Reve image generation response + // Format: { image: "base64...", version: "...", content_violation: false, ... } + if (usedProvider === "reve" && typeof json.image === "string") { + images = [ + { + type: "image_url", + image_url: { + url: `data:image/png;base64,${json.image}`, + }, + }, + ]; + content = imageLabel; + finishReason = "stop"; + promptTokens = 0; + completionTokens = 0; + totalTokens = 0; + break; + } // Check if this is an OpenAI responses format (has output array instead of choices) if (json.output && Array.isArray(json.output)) { // OpenAI responses endpoint format diff --git a/apps/gateway/src/chat/tools/transform-response-to-openai.ts b/apps/gateway/src/chat/tools/transform-response-to-openai.ts index 99dd2619a8..f763548053 100644 --- a/apps/gateway/src/chat/tools/transform-response-to-openai.ts +++ b/apps/gateway/src/chat/tools/transform-response-to-openai.ts @@ -1212,6 +1212,63 @@ export function transformResponseToOpenai( break; } default: { + // For providers that return non-OpenAI format (e.g. Reve image generation), + // construct a proper OpenAI-compatible response when we have parsed images/content + if ( + transformedResponse && + typeof transformedResponse === "object" && + !transformedResponse.choices && + (images.length > 0 || content !== null) + ) { + transformedResponse = { + id: `chatcmpl-${Date.now()}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model: formatUsedModelForDisplay( + usedProvider, + baseModelName, + undefined, + usedRegion, + ), + choices: [ + { + index: 0, + message: { + role: "assistant", + content: content, + ...(images && images.length > 0 && { images }), + }, + finish_reason: "stop", + }, + ], + usage: buildUsageObject( + promptTokens, + completionTokens, + totalTokens, + reasoningTokens, + cachedTokens, + costs, + showUpgradeMessage, + cacheCreationTokens, + imageInputTokens, + imageOutputTokens, + cacheCreation5mTokens, + cacheCreation1hTokens, + audioInputTokens, + ), + metadata: buildMetadata( + requestedModel, + requestedProvider, + baseModelName, + usedProvider, + usedModel, + requestId, + routing, + usedRegion, + ), + }; + break; + } // For any other provider, add metadata to existing response if (transformedResponse && typeof transformedResponse === "object") { // Ensure content and reasoning fields are present with parsed/healed values diff --git a/packages/actions/src/get-provider-endpoint.ts b/packages/actions/src/get-provider-endpoint.ts index 202eb5b8ee..69d029ef3d 100644 --- a/packages/actions/src/get-provider-endpoint.ts +++ b/packages/actions/src/get-provider-endpoint.ts @@ -251,6 +251,9 @@ export function getProviderEndpoint( case "minimax": url = "https://api.minimax.io"; break; + case "reve": + url = "https://api.reve.com"; + break; case "xiaomi": url = envValueOrDefault( @@ -567,6 +570,11 @@ export function getProviderEndpoint( return `${url}/v1/images/generations`; } return `${url}/v1/chat/completions`; + case "reve": + if (imageGenerations) { + return `${url}/v1/image/create`; + } + return `${url}/v1/image/create`; case "deepinfra": return `${url}/chat/completions`; case "inference.net": diff --git a/packages/actions/src/prepare-request-body.ts b/packages/actions/src/prepare-request-body.ts index 495e4b3189..1fe180f262 100644 --- a/packages/actions/src/prepare-request-body.ts +++ b/packages/actions/src/prepare-request-body.ts @@ -983,6 +983,50 @@ export async function prepareRequestBody( return alibabaImageRequest; } + // Handle Reve image generation + if (imageGenerations && usedProvider === "reve") { + const lastUserMessage = [...messages] + .reverse() + .find((m) => m.role === "user"); + let prompt = ""; + const imageUrls: string[] = []; + if (lastUserMessage) { + if (typeof lastUserMessage.content === "string") { + prompt = lastUserMessage.content; + } else if (Array.isArray(lastUserMessage.content)) { + for (const part of lastUserMessage.content) { + if (part.type === "text" && part.text) { + prompt += (prompt ? "\n" : "") + part.text; + } else if (part.type === "image_url" && part.image_url) { + const url = + typeof part.image_url === "string" + ? part.image_url + : part.image_url.url; + if (url) { + imageUrls.push(url); + } + } + } + } + } + + const reveRequest: any = { + prompt, + version: "latest", + ...(image_config?.aspect_ratio && { + aspect_ratio: image_config.aspect_ratio, + }), + }; + + if (imageUrls.length === 1) { + reveRequest.reference_image = imageUrls[0]; + } else if (imageUrls.length > 1) { + reveRequest.reference_images = imageUrls; + } + + return reveRequest; + } + // Handle ByteDance Seedream image generation if (imageGenerations && usedProvider === "bytedance") { // Extract prompt from last user message diff --git a/packages/models/src/models.ts b/packages/models/src/models.ts index 13954d55f2..a356dee014 100644 --- a/packages/models/src/models.ts +++ b/packages/models/src/models.ts @@ -12,6 +12,7 @@ import { moonshotModels } from "./models/moonshot.js"; import { nousresearchModels } from "./models/nousresearch.js"; import { openaiModels } from "./models/openai.js"; import { perplexityModels } from "./models/perplexity.js"; +import { reveModels } from "./models/reve.js"; import { xaiModels } from "./models/xai.js"; import { xiaomiModels } from "./models/xiaomi.js"; import { zaiModels } from "./models/zai.js"; @@ -547,5 +548,6 @@ export const models = [ ...alibabaModels, ...bytedanceModels, ...nousresearchModels, + ...reveModels, ...zaiModels, ] as const satisfies ModelDefinition[]; diff --git a/packages/models/src/models/reve.ts b/packages/models/src/models/reve.ts new file mode 100644 index 0000000000..8e366572e4 --- /dev/null +++ b/packages/models/src/models/reve.ts @@ -0,0 +1,30 @@ +import type { ModelDefinition } from "@/models.js"; + +export const reveModels = [ + { + id: "reve-create", + name: "Reve Create", + description: + "Reve's image generation model with native 4K resolution and code-based controllable image creation. Generates high-quality images from text prompts.", + family: "reve", + output: ["image"], + releasedAt: new Date("2025-09-15"), + providers: [ + { + test: "skip", + providerId: "reve", + externalId: "reve-create@20250915", + inputPrice: "0", + outputPrice: "0", + requestPrice: "0.024", + contextSize: 2560, + maxOutput: 1, + streaming: false, + vision: false, + tools: false, + jsonOutput: false, + imageGenerations: true, + }, + ], + }, +] as const satisfies ModelDefinition[]; diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index aae0cc0d9e..5230e0e3de 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -1103,6 +1103,26 @@ export const providers: ProviderDefinition[] = [ gdpr: true, }, }, + { + id: "reve", + name: "Reve", + description: + "Reve's image generation models with native 4K resolution and code-based controllable image creation.", + env: { + required: { + apiKey: "LLM_REVE_API_KEY", + }, + }, + streaming: false, + cancellation: false, + color: "#1a1a2e", + website: "https://reve.com", + announcement: null, + termsUrl: null, + privacyPolicyUrl: null, + headquarters: "US", + dataPolicy: null, + }, ] as const satisfies ProviderDefinition[]; export type ProviderId = (typeof providers)[number]["id"]; From 74fb7b6a43d53b9bb6a23e9f2397019272b1ba0f Mon Sep 17 00:00:00 2001 From: analogpvt Date: Thu, 4 Jun 2026 08:17:52 +0000 Subject: [PATCH 2/3] feat(reve): add terms and privacy policy URLs Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/models/src/providers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/models/src/providers.ts b/packages/models/src/providers.ts index 5230e0e3de..85f50ff9a0 100644 --- a/packages/models/src/providers.ts +++ b/packages/models/src/providers.ts @@ -1118,8 +1118,10 @@ export const providers: ProviderDefinition[] = [ color: "#1a1a2e", website: "https://reve.com", announcement: null, - termsUrl: null, - privacyPolicyUrl: null, + termsUrl: + "https://help.reve.com/hc/en-us/articles/46731550696468-Terms-of-service", + privacyPolicyUrl: + "https://help.reve.com/hc/en-us/articles/46731763484692-Privacy-policy", headquarters: "US", dataPolicy: null, }, From d9f12949e669653f1099dbff1493aa8dc11e5945 Mon Sep 17 00:00:00 2001 From: analogpvt Date: Thu, 4 Jun 2026 08:43:01 +0000 Subject: [PATCH 3/3] feat(xai): add grok-imagine-video-1.5-preview model Add xAI's video generation model with text-to-video and image-to-video support up to 15 seconds at $0.08/second. - Add model definition with per-second pricing - Add xAI video base URL (api.x.ai) - Add video job creation via /v1/videos/generations - Add image input support (first frame) - Enable direct upstream content proxy for xAI - Supported sizes: 1280x720, 1920x1080 - Supported durations: 5s, 10s, 15s Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/gateway/src/videos/videos.ts | 72 +++++++++++++++++++++++++- apps/worker/src/services/video-jobs.ts | 2 + packages/models/src/models/xai.ts | 34 ++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/apps/gateway/src/videos/videos.ts b/apps/gateway/src/videos/videos.ts index d31d3734cb..e6008b2c13 100644 --- a/apps/gateway/src/videos/videos.ts +++ b/apps/gateway/src/videos/videos.ts @@ -977,6 +977,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { switch (providerId) { case "openai": return "https://api.openai.com"; + case "xai": + return "https://api.x.ai"; case "bytedance": return "https://ark.ap-southeast.bytepluses.com/api/v3"; case "google-vertex": @@ -2148,7 +2150,7 @@ async function streamVideoFromUrl( } function shouldProxyDirectUpstreamVideoContent(job: VideoJobRecord): boolean { - return job.usedProvider === "openai"; + return job.usedProvider === "openai" || job.usedProvider === "xai"; } async function resolveVideoJobProviderContext(job: VideoJobRecord): Promise<{ @@ -2923,6 +2925,65 @@ async function createBytedanceVideoJob( return { upstreamId, upstreamRequest, upstreamResponse }; } +async function createXaiVideoJob( + providerContext: ProviderContext, + providerMapping: ProviderModelMapping, + videoSize: VideoSizeConfig, + prompt: string, + durationSeconds: number, + processedFirstFrame: ProcessedVideoImageInput | null, +): Promise<{ + upstreamId: string; + upstreamRequest: Record; + upstreamResponse: Record; +}> { + const upstreamModelName = providerMapping.externalId; + const upstreamRequest: Record = { + model: upstreamModelName, + prompt, + size: videoSize.size, + duration: durationSeconds, + }; + + if (processedFirstFrame) { + upstreamRequest.image = `data:${processedFirstFrame.mimeType};base64,${processedFirstFrame.bytesBase64Encoded}`; + } + + const upstreamUrl = joinUrl( + providerContext.baseUrl, + "/v1/videos/generations", + ); + const rawResponse = await fetchUpstreamJson(upstreamUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...getProviderHeaders("xai", providerContext.token, { + requestId: providerContext.requestId, + }), + }, + body: JSON.stringify(upstreamRequest), + }); + + const upstreamResponse = addRequestedVideoMetadata( + { + ...rawResponse, + model: upstreamModelName, + status: "queued", + duration: durationSeconds, + }, + videoSize, + ); + + const upstreamId = extractUpstreamVideoId(upstreamResponse); + if (!upstreamId) { + throw new HTTPException(502, { + message: "xAI video response did not include a request id", + }); + } + + return { upstreamId, upstreamRequest, upstreamResponse }; +} + async function createUpstreamVideoJob( providerContext: ProviderContext, providerMapping: ProviderModelMapping, @@ -2946,6 +3007,15 @@ async function createUpstreamVideoJob( upstreamResponse: Record; }> { switch (providerContext.providerId) { + case "xai": + return await createXaiVideoJob( + providerContext, + providerMapping, + videoSize, + prompt, + durationSeconds, + processedFirstFrame, + ); case "openai": return await createOpenAIVideoJob( providerContext, diff --git a/apps/worker/src/services/video-jobs.ts b/apps/worker/src/services/video-jobs.ts index d2fb29bd78..0f4983b339 100644 --- a/apps/worker/src/services/video-jobs.ts +++ b/apps/worker/src/services/video-jobs.ts @@ -136,6 +136,8 @@ function getDefaultVideoProviderBaseUrl(providerId: Provider): string | null { switch (providerId) { case "openai": return "https://api.openai.com"; + case "xai": + return "https://api.x.ai"; case "bytedance": return "https://ark.ap-southeast.bytepluses.com/api/v3"; case "google-vertex": diff --git a/packages/models/src/models/xai.ts b/packages/models/src/models/xai.ts index 8bf472ae21..47d578672f 100644 --- a/packages/models/src/models/xai.ts +++ b/packages/models/src/models/xai.ts @@ -802,6 +802,40 @@ export const xaiModels = [ }, ], }, + { + id: "grok-imagine-video-1-5-preview", + name: "Grok Imagine Video 1.5 Preview", + description: + "xAI's video generation model. Creates videos from text prompts and images with up to 15 seconds duration.", + family: "xai", + output: ["video"], + releasedAt: new Date("2026-05-01"), + providers: [ + { + test: "skip", + providerId: "xai", + externalId: "grok-imagine-video-1.5-preview", + inputPrice: "0", + outputPrice: "0", + requestPrice: "0", + perSecondPrice: { + "720p": "0.08", + "1080p": "0.08", + }, + contextSize: 2000, + maxOutput: 1, + streaming: false, + vision: true, + tools: false, + jsonOutput: false, + videoGenerations: true, + supportedVideoSizes: ["1280x720", "1920x1080"], + supportedVideoDurationsSeconds: [5, 10, 15], + supportsVideoAudio: false, + supportsVideoWithoutAudio: true, + }, + ], + }, { id: "grok-build-0-1", name: "Grok Build 0.1",