Skip to content
Merged
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
1 change: 1 addition & 0 deletions open-sse/executors/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ function clampEffort(model: string, requested: string): string {
function normalizeEffortValue(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
const normalized = value.trim().toLowerCase();
if (normalized === "max") return "xhigh";
return normalized || undefined;
}

Expand Down
109 changes: 89 additions & 20 deletions open-sse/handlers/imageGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ const STABILITY_EDIT_ENDPOINTS = {

const STABILITY_CONTROL_MODELS = new Set(["sketch", "structure", "style", "style-transfer"]);

function appendOptionalFormValue(formData, key, value) {
if (value === undefined || value === null || value === "") return;
formData.append(key, String(value));
}

function appendImageFormValue(formData, key, source, filename) {
formData.append(
key,
new Blob([source.buffer], {
type: source.contentType || "application/octet-stream",
}),
filename
);
}

const FAL_PRESET_SIZES = {
"1024x1024": "square_hd",
"512x512": "square",
Expand Down Expand Up @@ -1054,52 +1069,107 @@ async function handleStabilityAIImageGeneration({
? normalizeRequestedImageFormat(body, "png", ["png", "webp"])
: normalizeRequestedImageFormat(body, "png"),
};
const formData = new FormData();

if (body.prompt) upstreamBody.prompt = body.prompt;
if (body.negative_prompt) upstreamBody.negative_prompt = body.negative_prompt;
if (body.seed !== undefined) upstreamBody.seed = body.seed;
appendOptionalFormValue(formData, "output_format", upstreamBody.output_format);
if (body.prompt) {
upstreamBody.prompt = body.prompt;
appendOptionalFormValue(formData, "prompt", body.prompt);
}
if (body.negative_prompt) {
upstreamBody.negative_prompt = body.negative_prompt;
appendOptionalFormValue(formData, "negative_prompt", body.negative_prompt);
}
if (body.seed !== undefined) {
upstreamBody.seed = body.seed;
appendOptionalFormValue(formData, "seed", body.seed);
}

try {
if (STABILITY_GENERATION_ENDPOINTS[model]) {
if (model.startsWith("sd3.5")) {
upstreamBody.model = model;
appendOptionalFormValue(formData, "model", model);
}

if (imageUrl) {
const imageSource = await resolveImageSource(imageUrl);
upstreamBody.mode = "image-to-image";
upstreamBody.image = (await resolveImageSource(imageUrl)).base64;
if (body.strength !== undefined) upstreamBody.strength = body.strength;
appendOptionalFormValue(formData, "mode", "image-to-image");
upstreamBody.image = imageSource.base64;
appendImageFormValue(formData, "image", imageSource, "image");
if (body.strength !== undefined) {
upstreamBody.strength = body.strength;
appendOptionalFormValue(formData, "strength", body.strength);
}
} else {
upstreamBody.mode = "text-to-image";
appendOptionalFormValue(formData, "mode", "text-to-image");
}

if (!model.startsWith("sd3.5") || !imageUrl) {
upstreamBody.aspect_ratio = body.aspect_ratio || mapImageSize(body.size);
const aspectRatio = body.aspect_ratio || mapImageSize(body.size);
upstreamBody.aspect_ratio = aspectRatio;
appendOptionalFormValue(formData, "aspect_ratio", aspectRatio);
}

if (body.style_preset) upstreamBody.style_preset = body.style_preset;
if (body.style_preset) {
upstreamBody.style_preset = body.style_preset;
appendOptionalFormValue(formData, "style_preset", body.style_preset);
}
} else {
if (imageUrl) {
upstreamBody.image = (await resolveImageSource(imageUrl)).base64;
const imageSource = await resolveImageSource(imageUrl);
upstreamBody.image = imageSource.base64;
appendImageFormValue(formData, "image", imageSource, "image");
}

if (maskUrl && shouldIncludeStabilityMask(model)) {
upstreamBody.mask = (await resolveImageSource(maskUrl)).base64;
const maskSource = await resolveImageSource(maskUrl);
upstreamBody.mask = maskSource.base64;
appendImageFormValue(formData, "mask", maskSource, "mask");
}

if (body.search_prompt) upstreamBody.search_prompt = body.search_prompt;
if (body.grow_mask !== undefined) upstreamBody.grow_mask = body.grow_mask;
if (body.control_strength !== undefined)
if (body.search_prompt) {
upstreamBody.search_prompt = body.search_prompt;
appendOptionalFormValue(formData, "search_prompt", body.search_prompt);
}
if (body.grow_mask !== undefined) {
upstreamBody.grow_mask = body.grow_mask;
appendOptionalFormValue(formData, "grow_mask", body.grow_mask);
}
if (body.control_strength !== undefined) {
upstreamBody.control_strength = body.control_strength;
if (body.creativity !== undefined) upstreamBody.creativity = body.creativity;
if (body.left !== undefined) upstreamBody.left = body.left;
if (body.right !== undefined) upstreamBody.right = body.right;
if (body.up !== undefined) upstreamBody.up = body.up;
if (body.down !== undefined) upstreamBody.down = body.down;
if (body.style_preset) upstreamBody.style_preset = body.style_preset;
appendOptionalFormValue(formData, "control_strength", body.control_strength);
}
if (body.creativity !== undefined) {
upstreamBody.creativity = body.creativity;
appendOptionalFormValue(formData, "creativity", body.creativity);
}
if (body.left !== undefined) {
upstreamBody.left = body.left;
appendOptionalFormValue(formData, "left", body.left);
}
if (body.right !== undefined) {
upstreamBody.right = body.right;
appendOptionalFormValue(formData, "right", body.right);
}
if (body.up !== undefined) {
upstreamBody.up = body.up;
appendOptionalFormValue(formData, "up", body.up);
}
if (body.down !== undefined) {
upstreamBody.down = body.down;
appendOptionalFormValue(formData, "down", body.down);
}
if (body.style_preset) {
upstreamBody.style_preset = body.style_preset;
appendOptionalFormValue(formData, "style_preset", body.style_preset);
}

if (STABILITY_CONTROL_MODELS.has(model) && !upstreamBody.prompt) {
upstreamBody.prompt = body.prompt || "";
appendOptionalFormValue(formData, "prompt", body.prompt || "");
}
}

Expand All @@ -1111,11 +1181,10 @@ async function handleStabilityAIImageGeneration({
const response = await fetch(`${providerConfig.baseUrl.replace(/\/$/, "")}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(upstreamBody),
body: formData,
});

if (!response.ok) {
Expand Down
5 changes: 3 additions & 2 deletions open-sse/services/thinkingBudget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ function setCustomBudget(body, budget) {
};
}

// OpenAI reasoning_effort mapping (T11: add 'max' tier for full budget)
// OpenAI reasoning_effort mapping.
// GPT-5/Codex accepts xhigh for the top tier; keep full budget aligned.
if (result.reasoning_effort !== undefined || result.reasoning !== undefined) {
if (budget <= 0) {
delete result.reasoning_effort;
Expand All @@ -236,7 +237,7 @@ function setCustomBudget(body, budget) {
} else if (budget < 131072) {
result.reasoning_effort = "high";
} else {
result.reasoning_effort = "max"; // T11: full budget → "max"
result.reasoning_effort = "xhigh";
}
}

Expand Down
12 changes: 9 additions & 3 deletions open-sse/translator/request/claude-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
type JsonRecord = Record<string, unknown>;
const TOOL_CHOICE_ANY = ["a", "n", "y"].join("");

function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined {
if (typeof effort !== "string") return undefined;
const normalized = effort.toLowerCase();
if (normalized === "max") return "xhigh";
return normalized || undefined;
}
Comment on lines +8 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For robustness and consistency with the implementation in open-sse/executors/codex.ts, it is recommended to include .trim() when normalizing the effort string. This ensures that any leading or trailing whitespace provided by the client doesn't bypass the normalization logic.

Suggested change
function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined {
if (typeof effort !== "string") return undefined;
const normalized = effort.toLowerCase();
if (normalized === "max") return "xhigh";
return normalized || undefined;
}
function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined {
if (typeof effort !== "string") return undefined;
const normalized = effort.trim().toLowerCase();
if (normalized === "max") return "xhigh";
return normalized || undefined;
}


// Convert Claude request to OpenAI format
export function claudeToOpenAIRequest(model, body, stream) {
const result: {
Expand Down Expand Up @@ -105,8 +112,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
// Reasoning effort: map Claude-side thinking controls to OpenAI reasoning_effort.
// Priority: output_config.effort (Claude Code) > thinking.budget_tokens (Claude native).
// Budget buckets match the reverse mapping in thinkingBudget.ts::setCustomBudget.
const outputEffort =
typeof body.output_config?.effort === "string" ? body.output_config.effort.toLowerCase() : "";
const outputEffort = normalizeOpenAIReasoningEffort(body.output_config?.effort) || "";
if (outputEffort) {
result.reasoning_effort = outputEffort;
} else if (body.thinking?.type === "enabled" && typeof body.thinking.budget_tokens === "number") {
Expand All @@ -120,7 +126,7 @@ export function claudeToOpenAIRequest(model, body, stream) {
} else if (budget < 131072) {
result.reasoning_effort = "high";
} else {
result.reasoning_effort = "max";
result.reasoning_effort = "xhigh";
}
}

Expand Down
7 changes: 6 additions & 1 deletion open-sse/translator/request/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ function toString(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}

function normalizeResponsesReasoningEffort(value: unknown): string {
const effort = toString(value).toLowerCase();
return effort === "max" ? "xhigh" : effort;
}
Comment on lines +27 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

To ensure consistency across the codebase and improve robustness against client input variations, consider adding .trim() to the normalization logic here as well. This aligns with the behavior in open-sse/executors/codex.ts.

Suggested change
function normalizeResponsesReasoningEffort(value: unknown): string {
const effort = toString(value).toLowerCase();
return effort === "max" ? "xhigh" : effort;
}
function normalizeResponsesReasoningEffort(value: unknown): string {
const effort = toString(value).trim().toLowerCase();
return effort === "max" ? "xhigh" : effort;
}


function unsupportedFeature(message: string): Error & { statusCode: number; errorType: string } {
const error = new Error(message) as Error & { statusCode: number; errorType: string };
error.statusCode = 400;
Expand Down Expand Up @@ -558,7 +563,7 @@ export function openaiToOpenAIResponsesRequest(
if (root.reasoning !== undefined) {
result.reasoning = root.reasoning;
} else if (root.reasoning_effort !== undefined) {
const effort = toString(root.reasoning_effort);
const effort = normalizeResponsesReasoningEffort(root.reasoning_effort);
if (effort) {
result.reasoning = { effort };
}
Expand Down
29 changes: 27 additions & 2 deletions src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,35 @@ function getVoiceList(providerId: string) {

/** Parse a human-readable error from the API error response */
function parseApiError(raw: any, statusCode: number): { message: string; isCredentials: boolean } {
const readErrorMessage = (value: any): string | null => {
if (!value) return null;
if (typeof value === "string") return value;
if (Array.isArray(value)) {
const messages = value
.map((entry: any) => readErrorMessage(entry))
.filter((entry: string | null): entry is string => Boolean(entry));
if (messages.length > 0) return messages.join(", ");
return null;
}
if (typeof value.message === "string") return value.message;
if (typeof value.detail === "string") return value.detail;
if (Array.isArray(value.errors)) {
const messages = value.errors
.map((entry: any) => readErrorMessage(entry))
.filter((entry: string | null): entry is string => Boolean(entry));
if (messages.length > 0) return messages.join(", ");
}
try {
return JSON.stringify(value);
} catch {
return null;
}
};

const msg =
raw?.error?.message ||
readErrorMessage(raw?.error) ||
readErrorMessage(raw?.errors) ||
raw?.err_msg ||
raw?.error ||
raw?.message ||
raw?.detail ||
(typeof raw === "string" ? raw : null) ||
Expand Down
Loading