Files
OmniRoute/open-sse/handlers/imageGeneration/providers/designerWeb.ts
backryun d628105503 refactor(sse): retag the designer-web result unions with string discriminants (#8531)
All 10 diagnostics in this file are the `strictNullChecks: false` narrowing
limitation: a boolean-literal discriminant narrows the positive branch but
leaves the negative one as the full union, so `if (!resolved.ok)` and the code
after `if (outcome.success)` could not see `status`/`error`.

Three unions, all module-private and untouched by any test, so per the rule
recorded on #8499 these are retagged rather than fixed with predicates —
predicates are for exported unions whose shape callers depend on:

  resolveDesignerWebRequest  ok: true|false      -> state: "resolved"|"invalid"
  DesignerWebStepResult      done + success      -> state: "pending"|"ready"|"failed"

The step union collapsed two booleans into one discriminant; `done`/`success`
encoded three states across two flags, which is also why the pending arm had no
`success` property for `outcome.success` to read.

Also narrowed runDesignerWebPollLoop's declared return from
`DesignerWebStepResult | {…504…}` to a new DesignerWebOutcome (ready | failed).
The loop returns a step only after confirming it is terminal, and otherwise
synthesizes a 504 — it can never return a pending step, and the old signature
claiming it could is what made `.success` unreadable on the union at all.

280 -> 270, zero new, on a line-number-agnostic diff of the full tsc error set.

Tests: unlike the previous slices this rewrote real control flow (three
conditionals), so the existing suite is doing actual work here —
microsoft-designer-web-6672.test.ts drives the handler end-to-end through 400,
401, immediate-ready, poll-then-ready, non-OK upstream and 504-timeout, i.e.
every arm but one. The "empty" arm (unrecognized 200 body -> terminal 502) was
tested only at the parser level, never through the handler, so the 502 itself
was unasserted. Added designer-web-empty-response-502.test.ts (2 tests) pinning
that it is terminal (exactly one fetch, no polling) and distinct from the 504
deadline path. Both suites pass against the parent commit too — the tests are
black-box through the exported handler, so they are agnostic to the discriminant
rename and prove the retag is behavior-preserving. 61/61 across the 3 suites.

Co-authored-by: backryun <busan011@ormbiz.co.kr>
2026-07-26 03:52:00 -03:00

302 lines
10 KiB
TypeScript

// Microsoft Designer (unofficial, reverse-engineered web API) image handler.
// Family: designer-web | Provider: microsoft-designer-web
// Reference: g4f/Provider/needs_auth/MicrosoftDesigner.py (fetched + verified
// during triage of #6672) — Bearer access_token auth against
// designerapp.officeapps.live.com/designerapp/DallE.ashx, submit-then-poll
// for image_urls_thumbnail[].ImageUrl.
//
// The upstream ClientId header is a fixed, publicly-shared value the
// designer.microsoft.com frontend sends on every session (not a secret) —
// routed through resolvePublicCred() per Hard Rule #11 / docs/security/PUBLIC_CREDS.md.
import { randomUUID, randomBytes } from "node:crypto";
import { resolvePublicCred } from "../../../utils/publicCreds.ts";
import { sanitizeErrorMessage } from "../../../utils/error.ts";
import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGeneration.ts";
const DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT = 60000;
const DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT = 2000;
const DESIGNER_WEB_BATCH_SIZE = "4";
/** Maps an OpenAI-style "WxH" size string to the closest Designer aspect ratio bucket. */
export function mapDesignerWebImageSize(size: unknown): "1_1" | "16_9" | "9_16" {
if (typeof size !== "string" || !size.includes("x")) return "1_1";
const [wRaw, hRaw] = size.split("x");
const w = Number(wRaw);
const h = Number(hRaw);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return "1_1";
if (w > h * 1.2) return "16_9";
if (h > w * 1.2) return "9_16";
return "1_1";
}
/** Builds the fixed + per-request headers Microsoft Designer expects on every call. */
export function buildDesignerWebHeaders({
accessToken,
sessionId = randomUUID(),
userId = randomBytes(16).toString("hex"),
}: {
accessToken: string;
sessionId?: string;
userId?: string;
}): Record<string, string> {
return {
Authorization: `Bearer ${accessToken}`,
ClientId: resolvePublicCred("microsoft_designer_client_id"),
SessionId: sessionId,
UserId: userId,
"Content-Type": "application/x-www-form-urlencoded",
};
}
/** Builds the DallE.ashx form body from an OpenAI-shaped image-generation request. */
export function buildDesignerWebFormBody(prompt: string, size: unknown): URLSearchParams {
const params = new URLSearchParams();
params.set("dalle-caption", prompt);
params.set("dalle-image-size", mapDesignerWebImageSize(size));
params.set("dalle-batch-size", DESIGNER_WEB_BATCH_SIZE);
params.set("dalle-seed", String(Math.floor(Math.random() * 1_000_000_000)));
return params;
}
interface DesignerWebParsedResponse {
status: "ready" | "pending" | "empty";
imageUrls: string[];
pollIntervalMs: number | null;
}
/** Parses a DallE.ashx JSON body into a ready/pending/empty verdict. */
export function parseDesignerWebResponse(json: unknown): DesignerWebParsedResponse {
const body = (json ?? {}) as Record<string, unknown>;
const thumbs = Array.isArray(body.image_urls_thumbnail) ? body.image_urls_thumbnail : [];
const imageUrls = thumbs
.map((t) => (t && typeof t === "object" ? (t as Record<string, unknown>).ImageUrl : null))
.filter((u): u is string => typeof u === "string" && u.length > 0);
if (imageUrls.length > 0) {
return { status: "ready", imageUrls, pollIntervalMs: null };
}
const pollingMeta = (body.polling_response as Record<string, unknown> | undefined)
?.polling_meta_data as Record<string, unknown> | undefined;
const pollIntervalMs = Number.isFinite(pollingMeta?.poll_interval)
? Number(pollingMeta?.poll_interval)
: null;
if (pollIntervalMs !== null) {
return { status: "pending", imageUrls: [], pollIntervalMs };
}
return { status: "empty", imageUrls: [], pollIntervalMs: null };
}
function normalizePositiveNumber(value: unknown, fallback: number): number {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? n : fallback;
}
interface DesignerWebRequestConfig {
prompt: string;
accessToken: string;
headers: Record<string, string>;
formBody: URLSearchParams;
timeoutMs: number;
pollIntervalMs: number;
}
/**
* Outcome of request validation. String-discriminated rather than `ok: boolean`
* because `open-sse` compiles with `strictNullChecks: false`, where a
* boolean-literal discriminant narrows the positive branch but leaves the
* negative one as the full union — so `if (!resolved.ok)` would not expose
* `status`/`error`. All three unions in this file shared that root cause.
*/
type DesignerWebRequestResolution =
| { state: "resolved"; config: DesignerWebRequestConfig }
| { state: "invalid"; status: number; error: string };
/** Validates the request and resolves auth + poll timing. Returns an error status/message on failure. */
function resolveDesignerWebRequest(
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown },
credentials: { apiKey?: string; accessToken?: string }
): DesignerWebRequestResolution {
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
if (!prompt) {
return {
state: "invalid",
status: 400,
error: "Prompt is required for Microsoft Designer image generation",
};
}
const accessToken = credentials?.apiKey || credentials?.accessToken;
if (!accessToken) {
return {
state: "invalid",
status: 401,
error: "Microsoft Designer credentials missing access_token",
};
}
const timeoutMs = normalizePositiveNumber(
body.timeout_ms,
normalizePositiveNumber(
process.env.DESIGNER_WEB_POLL_TIMEOUT_MS,
DESIGNER_WEB_POLL_TIMEOUT_MS_DEFAULT
)
);
const pollIntervalMs = normalizePositiveNumber(
body.poll_interval_ms,
normalizePositiveNumber(
process.env.DESIGNER_WEB_POLL_INTERVAL_MS,
DESIGNER_WEB_POLL_INTERVAL_MS_DEFAULT
)
);
return {
state: "resolved",
config: {
prompt,
accessToken,
headers: buildDesignerWebHeaders({ accessToken }),
formBody: buildDesignerWebFormBody(prompt, body.size),
timeoutMs,
pollIntervalMs,
},
};
}
type DesignerWebPending = { state: "pending"; waitMs: number };
type DesignerWebReady = { state: "ready"; imageUrls: string[] };
type DesignerWebFailed = { state: "failed"; status: number; error: string };
/** One poll cycle: still working, finished with images, or finished with an error. */
type DesignerWebStepResult = DesignerWebPending | DesignerWebReady | DesignerWebFailed;
/**
* What the poll loop hands back. Deliberately excludes the pending arm — the
* loop either returns a terminal step or synthesizes a 504, and never surfaces
* `pending` to its caller. The previous signature admitted it, which is why
* `outcome.success` did not exist on every member of that union.
*/
type DesignerWebOutcome = DesignerWebReady | DesignerWebFailed;
/** Runs one submit/poll fetch cycle and classifies the outcome. */
async function stepDesignerWebPoll(
baseUrl: string,
headers: Record<string, string>,
formBody: URLSearchParams,
pollIntervalMs: number,
fetchImpl: typeof fetch
): Promise<DesignerWebStepResult> {
const resp = await fetchImpl(baseUrl, { method: "POST", headers, body: formBody });
if (!resp.ok) {
return {
state: "failed",
status: resp.status,
error: sanitizeErrorMessage(await resp.text()),
};
}
const parsed = parseDesignerWebResponse(await resp.json());
if (parsed.status === "ready") {
return { state: "ready", imageUrls: parsed.imageUrls };
}
if (parsed.status === "empty") {
return {
state: "failed",
status: 502,
error: "Microsoft Designer response did not contain image data or polling metadata",
};
}
return {
state: "pending",
waitMs: Math.min(parsed.pollIntervalMs ?? pollIntervalMs, pollIntervalMs),
};
}
/** Drives the submit-then-poll loop to completion, timeout, or a terminal error. */
async function runDesignerWebPollLoop(
baseUrl: string,
config: DesignerWebRequestConfig,
fetchImpl: typeof fetch,
log?: { info?: (...args: unknown[]) => void }
): Promise<DesignerWebOutcome> {
const deadline = Date.now() + config.timeoutMs;
let attempt = 0;
while (Date.now() < deadline) {
attempt += 1;
const step = await stepDesignerWebPoll(
baseUrl,
config.headers,
config.formBody,
config.pollIntervalMs,
fetchImpl
);
if (step.state !== "pending") return step;
log?.info?.("IMAGE", `designer-web pending, poll #${attempt} in ${step.waitMs}ms`);
await new Promise((resolve) => setTimeout(resolve, step.waitMs));
}
return {
state: "failed",
status: 504,
error: "Microsoft Designer image generation timed out waiting for a result",
};
}
export async function handleDesignerWebImageGeneration({
model,
provider,
providerConfig,
body,
credentials,
log,
fetchImpl = fetch,
}: {
model: string;
provider: string;
providerConfig: { baseUrl: string };
body: { prompt?: unknown; size?: unknown; timeout_ms?: unknown; poll_interval_ms?: unknown };
credentials: { apiKey?: string; accessToken?: string };
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
fetchImpl?: typeof fetch;
}) {
const startTime = Date.now();
const resolved = resolveDesignerWebRequest(body, credentials);
if (resolved.state === "invalid") {
return saveImageErrorResult({
provider,
model,
status: resolved.status,
startTime,
error: resolved.error,
});
}
try {
const outcome = await runDesignerWebPollLoop(providerConfig.baseUrl, resolved.config, fetchImpl, log);
if (outcome.state === "ready") {
return saveImageSuccessResult({
provider,
model,
startTime,
images: outcome.imageUrls.map((url) => ({ url })),
});
}
if (log?.error) {
log.error("IMAGE", `${provider} designer-web error ${outcome.status}: ${outcome.error}`);
}
return saveImageErrorResult({ provider, model, status: outcome.status, startTime, error: outcome.error });
} catch (err) {
const errorText = sanitizeErrorMessage(err instanceof Error ? err.message : String(err));
if (log?.error) {
log.error("IMAGE", `${provider} designer-web exception: ${errorText}`);
}
return saveImageErrorResult({ provider, model, status: 500, startTime, error: errorText });
}
}