Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 18 additions & 0 deletions apps/gateway/src/chat/tools/parse-provider-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions apps/gateway/src/chat/tools/transform-response-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 71 additions & 1 deletion apps/gateway/src/videos/videos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -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<string, unknown>;
upstreamResponse: Record<string, unknown>;
}> {
const upstreamModelName = providerMapping.externalId;
const upstreamRequest: Record<string, unknown> = {
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,
Expand All @@ -2946,6 +3007,15 @@ async function createUpstreamVideoJob(
upstreamResponse: Record<string, unknown>;
}> {
switch (providerContext.providerId) {
case "xai":
return await createXaiVideoJob(
providerContext,
providerMapping,
videoSize,
prompt,
durationSeconds,
processedFirstFrame,
);
case "openai":
return await createOpenAIVideoJob(
providerContext,
Expand Down
2 changes: 2 additions & 0 deletions apps/worker/src/services/video-jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
8 changes: 8 additions & 0 deletions packages/actions/src/get-provider-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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":
Expand Down
44 changes: 44 additions & 0 deletions packages/actions/src/prepare-request-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/models/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -547,5 +548,6 @@ export const models = [
...alibabaModels,
...bytedanceModels,
...nousresearchModels,
...reveModels,
...zaiModels,
] as const satisfies ModelDefinition[];
30 changes: 30 additions & 0 deletions packages/models/src/models/reve.ts
Original file line number Diff line number Diff line change
@@ -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[];
34 changes: 34 additions & 0 deletions packages/models/src/models/xai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions packages/models/src/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,28 @@ 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:
"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,
},
] as const satisfies ProviderDefinition[];

export type ProviderId = (typeof providers)[number]["id"];
Expand Down
Loading