mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* feat(sse): add Microsoft Designer as image provider (#6672) Adds `microsoft-designer-web` — an unofficial, reverse-engineered Bearer-token web-session image provider, modeled on the existing `chatgpt-web`/`copilot-m365-web` "-web" provider category. - Registers the provider in WEB_COOKIE_PROVIDERS (src/shared/constants/ providers/web-cookie.ts) and IMAGE_PROVIDERS (open-sse/config/ imageRegistry.ts, new "designer-web" format). - New handler open-sse/handlers/imageGeneration/providers/designerWeb.ts implements the submit-then-poll DallE.ashx flow (Bearer access_token + ClientId/SessionId/UserId headers -> form POST -> poll for image_urls_thumbnail), wired into handleImageGeneration()'s dispatch. - The upstream ClientId header is a fixed, publicly-shared value (not a secret) — routed through resolvePublicCred() per Hard Rule #11, never as a string literal. - Registers the token-based credential requirement in webSessionCredentials.ts so the provider-connect UI asks for the right field; connection validation falls back to the existing generic web-cookie session-ping validator (no dedicated validator needed). - Extracted the KIE image-model catalog into a co-located open-sse/config/providers/registry/kie/models.ts module (mirrors the existing lmarena/directModels.ts pattern) to keep imageRegistry.ts under the file-size cap while adding the new provider entry. - Regenerated docs/reference/PROVIDER_REFERENCE.md (250 -> 251 providers) and updated the plain-text counts in README.md, AGENTS.md, CLAUDE.md. Tests (tests/unit/microsoft-designer-web-6672.test.ts, 16 cases): registry-entry shape assertions, the resolvePublicCred() shape assertion (Hard Rule #11), and the pure header/form-body/response- parsing helpers plus the handler's submit/poll/error/timeout paths against a mocked fetch — no live Designer session required. Reverse-engineered from the g4f MicrosoftDesigner.py provider reference (researched during #6672 triage); the exact upstream response shape has not been validated against a live Designer session, so the poll-loop implementation follows the documented g4f contract as closely as possible without a live capture. * fix(providers): satisfy web-cookie executor contract + document designer-web env vars (#6672)
This commit is contained in:
committed by
GitHub
parent
b0b40a3283
commit
5bacb719d3
@@ -1425,6 +1425,12 @@ APP_LOG_TO_FILE=true
|
||||
# NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s)
|
||||
# NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s)
|
||||
|
||||
# ── Microsoft Designer Web (Image Generation) ──
|
||||
# Polling config for the microsoft-designer-web submit-then-poll image job.
|
||||
# Used by: open-sse/handlers/imageGeneration/providers/designerWeb.ts
|
||||
# DESIGNER_WEB_POLL_TIMEOUT_MS=60000 # Max wait for job completion (default: 60s)
|
||||
# DESIGNER_WEB_POLL_INTERVAL_MS=2000 # Poll frequency (default: 2s)
|
||||
|
||||
# ── AWS Bedrock (Kiro / Audio) ──
|
||||
# Region used to construct AWS Bedrock endpoints. Used by:
|
||||
# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- feat(sse): add Microsoft Designer as an unofficial web-session image provider, reverse-engineered submit-then-poll DallE.ashx flow (#6672)
|
||||
@@ -801,6 +801,8 @@ Automatic model pricing data synchronization from external sources.
|
||||
| `MODEL_CATALOG_INCLUDE_NAMES` | `true` | `src/shared/constants/featureFlagDefinitions.ts` | Include display-friendly `name` fields in `/v1/models` responses. Disable for clients that expect IDs only. |
|
||||
| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. |
|
||||
| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. |
|
||||
| `DESIGNER_WEB_POLL_TIMEOUT_MS` | `60000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | Max wait for microsoft-designer-web image generation jobs. |
|
||||
| `DESIGNER_WEB_POLL_INTERVAL_MS` | `2000` | `open-sse/handlers/imageGeneration/providers/designerWeb.ts` | microsoft-designer-web job polling frequency. |
|
||||
| `AWS_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Region used to construct AWS Bedrock endpoints (Kiro, audio). |
|
||||
| `AWS_DEFAULT_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Fallback when `AWS_REGION` is not set. |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. |
|
||||
|
||||
@@ -10,7 +10,7 @@ lastUpdated: 2026-07-17
|
||||
> Regenerate with: `npm run gen:provider-reference`
|
||||
> **Last generated:** 2026-07-17
|
||||
|
||||
Total providers: **256**. See category breakdown below.
|
||||
Total providers: **258**. See category breakdown below.
|
||||
|
||||
## Categories
|
||||
|
||||
@@ -58,7 +58,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `zed` | `zd` | Zed IDE | OAuth | [link](https://zed.dev) | Zed stores LLM provider credentials (OpenAI, Anthropic, Google, Mistral, xAI) in the OS keychain. Use the Import button below to discover and import them automatically. |
|
||||
| `zed-hosted` | — | Zed Hosted Models | OAuth | [link](https://zed.dev) | Sign in with your Zed account (native-app sign-in). OmniRoute generates a one-time RSA keypair and opens zed.dev to authorize it — on a remote/headless install, copy the resulting 127.0.0.1 callback URL from your browser's address bar and paste it back here. Distinct from the 'Zed IDE' credential-import entry above: this proxies chat completions through Zed's own hosted model aggregator (cloud.zed.dev), fronting Anthropic/OpenAI/Google/xAI models under your Zed plan. |
|
||||
|
||||
## Web Cookie Providers (26)
|
||||
## Web Cookie Providers (27)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -77,6 +77,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `inner-ai` | `in-ai` | Inner.ai (Subscription) | Web cookie | [link](https://app.innerai.com) | Paste your token cookie and email separated by a space: open DevTools → Application → Cookies → .innerai.com, copy the token value, then append a space and your Inner.ai login email. Example: eyJhbG... user@example.com |
|
||||
| `kimi-web` | `kimi-web` | Kimi Web (Moonshot AI) | Web cookie | [link](https://www.kimi.com) | Paste your Cookie header from www.kimi.com (must contain kimi-auth=...). Find it via DevTools → Network → request → Cookie. |
|
||||
| `lmarena` | `lma` | Arena (Free) | Web cookie | [link](https://arena.ai) | Paste the full Cookie header from arena.ai (DevTools → Network → request → Cookie). Include arena-auth-prod-v1.0/.1… and cf_clearance/__cf_bm when present. OmniRoute uses Chrome TLS impersonation; if Arena still 403s, set providerSpecificData.recaptchaV3Token from a live browser session. |
|
||||
| `microsoft-designer-web` | `msdesigner` | Microsoft Designer (Image Generation) | Web cookie | [link](https://designer.microsoft.com) | Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration. |
|
||||
| `muse-spark-web` | `ms-web` | Muse Spark Web (Meta AI) | Web cookie | [link](https://www.meta.ai) | Paste your ecto_1_sess value or full cookie header from meta.ai |
|
||||
| `notion-web` | `nw` | Notion AI Web (Unofficial/Experimental) | Web cookie | [link](https://www.notion.so) | Paste your token_v2 cookie value from notion.so (DevTools → Application → Cookies). Optionally append `; space_id=...` and/or `; notion_browser_id=...` if your workspace requires them. |
|
||||
| `perplexity-web` | `pplx-web` | Perplexity Web (Pro/Max) | Web cookie | [link](https://www.perplexity.ai) | Paste your __Secure-next-auth.session-token cookie value from perplexity.ai |
|
||||
@@ -89,7 +90,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `zai-web` | `zw` | Z.ai Web (Free) | Web cookie | [link](https://chat.z.ai) | Paste the full Cookie header from chat.z.ai (must include the token=<JWT> cookie) |
|
||||
| `zenmux-free` | `zmf` | ZenMux Free (Web) | Web cookie | [link](https://zenmux.ai) | Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days. |
|
||||
|
||||
## API Key Providers (paid / paid-with-free-credits) (170)
|
||||
## API Key Providers (paid / paid-with-free-credits) (171)
|
||||
|
||||
| ID | Alias | Name | Tags | Website | Notes |
|
||||
|----|-------|------|------|---------|-------|
|
||||
@@ -145,6 +146,7 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each
|
||||
| `fireworks` | `fireworks` | Fireworks AI | API key | [link](https://fireworks.ai) | $1 free starter credits on signup for API testing |
|
||||
| `freeaiapikey` | `faik` | FreeAIAPIKey | API key | [link](https://freeaiapikey.com) | — |
|
||||
| `freemodel-dev` | `fmd` | FreeModel.dev | API key | [link](https://freemodel.dev) | $300 free credits on signup — no credit card required. Access GPT-5.4 and GPT-5.5 (OpenAI's latest flagship models) through an OpenAI-compatible API. |
|
||||
| `freepik` | `fpk` | Freepik (Mystic) | API key, image | [link](https://freepik.com) | Get API key at freepik.com/developers (Mystic image endpoint) |
|
||||
| `freetheai` | `fta` | FreeTheAi | API key, aggregator | [link](https://freetheai.xyz) | Join the FreeTheAi Discord to get your free API key. |
|
||||
| `friendliai` | `friendli` | FriendliAI | API key | [link](https://friendli.ai) | Free tier for serverless inference — no credit card required |
|
||||
| `galadriel` | `galadriel` | Galadriel | API key | [link](https://galadriel.com) | ⚠️ **DEPRECATED.** api.galadriel.ai no longer resolves (sweep 2026-06-19); the inference API appears discontinued. |
|
||||
|
||||
@@ -170,6 +170,17 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "1024x1536", "1536x1024"],
|
||||
},
|
||||
|
||||
"microsoft-designer-web": {
|
||||
id: "microsoft-designer-web",
|
||||
alias: "msdesigner",
|
||||
baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "designer-web",
|
||||
models: [{ id: "dall-e-3", name: "DALL-E 3 (Microsoft Designer Web)" }],
|
||||
supportedSizes: ["1024x1024", "1792x1024", "1024x1792"],
|
||||
},
|
||||
|
||||
xai: {
|
||||
id: "xai",
|
||||
baseUrl: "https://api.x.ai/v1/images/generations",
|
||||
|
||||
43
open-sse/config/providers/registry/kie/models.ts
Normal file
43
open-sse/config/providers/registry/kie/models.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* KIE image-provider model catalog — extracted from imageRegistry.ts
|
||||
* (god-file decomposition, mirrors the lmarena/directModels.ts pattern).
|
||||
* Pure data literal; imported by imageRegistry.ts. No behavior change.
|
||||
*/
|
||||
export const KIE_IMAGE_MODELS = [
|
||||
{ id: "gpt4o-image", name: "KIE 4o Image" },
|
||||
{ id: "seedream/4.5-text-to-image", name: "Seedream 4.5", isMarket: true },
|
||||
{ id: "seedream/4.5-edit", name: "Seedream 4.5 Edit", isMarket: true },
|
||||
{ id: "seedream/5.0-lite-text-to-image", name: "Seedream 5.0 Lite", isMarket: true },
|
||||
{ id: "seedream/5.0-lite-image-to-image", name: "Seedream 5.0 Lite I2I", isMarket: true },
|
||||
{ id: "z-image/4.0-text-to-image", name: "Z-Image v4.0", isMarket: true },
|
||||
{ id: "z-image/4.5-text-to-image", name: "Z-Image v4.5", isMarket: true },
|
||||
{ id: "google-imagen/imagen4-fast", name: "Imagen 4 Fast", isMarket: true },
|
||||
{ id: "google-imagen/imagen4-ultra", name: "Imagen 4 Ultra", isMarket: true },
|
||||
{ id: "google-imagen/imagen4", name: "Imagen 4", isMarket: true },
|
||||
{ id: "google-imagen/nano-banana-2", name: "Nano Banana 2", isMarket: true },
|
||||
{ id: "google-imagen/nano-banana", name: "Nano Banana", isMarket: true },
|
||||
{ id: "google-imagen/nano-banana-pro", name: "Nano Banana Pro", isMarket: true },
|
||||
{ id: "google-imagen/nano-banana-edit", name: "Nano Banana Edit", isMarket: true },
|
||||
{ id: "flux/2-pro-image-to-image", name: "Flux 2 Pro I2I", isMarket: true },
|
||||
{ id: "flux/2-pro-text-to-image", name: "Flux 2 Pro T2I", isMarket: true },
|
||||
{ id: "flux/2-image-to-image", name: "Flux 2 I2I", isMarket: true },
|
||||
{ id: "flux/2-text-to-image", name: "Flux 2 T2I", isMarket: true },
|
||||
{ id: "flux/kontext", name: "Flux Kontext", isMarket: true },
|
||||
{ id: "grok-imagine/text-to-image", name: "Grok Imagine T2I", isMarket: true },
|
||||
{ id: "grok-imagine/image-to-image", name: "Grok Imagine I2I", isMarket: true },
|
||||
{ id: "gpt/gpt-image-1.5-text-to-image", name: "GPT Image 1.5 T2I", isMarket: true },
|
||||
{ id: "gpt/gpt-image-1.5-image-to-image", name: "GPT Image 1.5 I2I", isMarket: true },
|
||||
{ id: "gpt/gpt-image-2-text-to-image", name: "GPT Image 2 T2I", isMarket: true },
|
||||
{ id: "gpt/gpt-image-2-image-to-image", name: "GPT Image 2 I2I", isMarket: true },
|
||||
{ id: "ideogram/v3-text-to-image", name: "Ideogram v3", isMarket: true },
|
||||
{ id: "ideogram/v3-edit", name: "Ideogram v3 Edit", isMarket: true },
|
||||
{ id: "ideogram/v3-remix", name: "Ideogram v3 Remix", isMarket: true },
|
||||
{ id: "ideogram/v3-reframe", name: "Ideogram v3 Reframe", isMarket: true },
|
||||
{ id: "qwen/text-to-image", name: "Qwen T2I", isMarket: true },
|
||||
{ id: "qwen/image-to-image", name: "Qwen I2I", isMarket: true },
|
||||
{ id: "qwen/image-edit", name: "Qwen Edit", isMarket: true },
|
||||
{ id: "qwen2/image-edit", name: "Qwen2 Edit", isMarket: true },
|
||||
{ id: "qwen2/text-to-image", name: "Qwen2 T2I", isMarket: true },
|
||||
{ id: "wan/2.7-image", name: "Wan 2.7 Image", isMarket: true },
|
||||
{ id: "wan/2.7-image-pro", name: "Wan 2.7 Image Pro", isMarket: true },
|
||||
];
|
||||
@@ -36,6 +36,7 @@ import { AdaptaWebExecutor } from "./adapta-web.ts";
|
||||
import { ClaudeWebWithAutoRefresh } from "./claude-web-with-auto-refresh.ts";
|
||||
import { CopilotWebExecutor } from "./copilot-web.ts";
|
||||
import { CopilotM365WebExecutor } from "./copilot-m365-web.ts";
|
||||
import { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts";
|
||||
import { VeoAIFreeWebExecutor } from "./veoaifree-web.ts";
|
||||
import { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts";
|
||||
import { FeloWebExecutor } from "./felo-web.ts";
|
||||
@@ -125,6 +126,8 @@ const executors = {
|
||||
"copilot-web": new CopilotWebExecutor(),
|
||||
"copilot-m365-web": new CopilotM365WebExecutor(),
|
||||
copilot: new CopilotWebExecutor(), // Alias
|
||||
"microsoft-designer-web": new MicrosoftDesignerWebExecutor(),
|
||||
msdesigner: new MicrosoftDesignerWebExecutor(), // Alias
|
||||
"veoaifree-web": new VeoAIFreeWebExecutor(),
|
||||
"veo-free": new VeoAIFreeWebExecutor(), // Alias
|
||||
"duckduckgo-web": new DuckDuckGoWebExecutor(),
|
||||
@@ -239,6 +242,7 @@ export { DevinCliExecutor } from "./devin-cli.ts";
|
||||
export { AuggieExecutor } from "./auggie.ts";
|
||||
export { CopilotWebExecutor } from "./copilot-web.ts";
|
||||
export { CopilotM365WebExecutor } from "./copilot-m365-web.ts";
|
||||
export { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts";
|
||||
export { VeoAIFreeWebExecutor } from "./veoaifree-web.ts";
|
||||
export { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts";
|
||||
export { FeloWebExecutor } from "./felo-web.ts";
|
||||
|
||||
51
open-sse/executors/microsoft-designer-web.ts
Normal file
51
open-sse/executors/microsoft-designer-web.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// MicrosoftDesignerWebExecutor — chat-completions guard for the
|
||||
// microsoft-designer-web web-cookie provider (#6672).
|
||||
//
|
||||
// Microsoft Designer (designerapp.officeapps.live.com/DallE.ashx) is an
|
||||
// image-generation-only upstream: it has no chat/completions surface at all.
|
||||
// The real request/response flow lives entirely in the image-generation
|
||||
// handler (open-sse/handlers/imageGeneration/providers/designerWeb.ts),
|
||||
// dispatched from open-sse/handlers/imageGeneration.ts by
|
||||
// providerConfig.format === "designer-web" — NOT through getExecutor().
|
||||
//
|
||||
// microsoft-designer-web is still listed in WEB_COOKIE_PROVIDERS (it uses
|
||||
// the same unofficial, DevTools-sourced bearer-token credential UX and
|
||||
// subscription-risk notice as the other web-cookie providers — see
|
||||
// tests/unit/microsoft-designer-web-6672.test.ts). Without a registered
|
||||
// executor here, getExecutor("microsoft-designer-web") silently falls
|
||||
// through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai`
|
||||
// fallback (open-sse/executors/index.ts:176 comment, #6699) — which would
|
||||
// send the user's real Designer bearer token to api.openai.com, mislabeled
|
||||
// as an OpenAI request, if anything ever mis-routes a chat/completions call
|
||||
// to this provider.
|
||||
//
|
||||
// This executor closes that gap cheaply: it never calls the network. Any
|
||||
// chat/completions attempt against microsoft-designer-web is rejected
|
||||
// immediately with a clean, sanitized 400 telling the caller to use
|
||||
// /v1/images/generations instead — satisfying the executor wrapper
|
||||
// contract (tests/unit/executor-web-cookie-sweep.test.ts) without ever
|
||||
// forwarding credentials anywhere.
|
||||
import { BaseExecutor, type ExecuteInput } from "./base.ts";
|
||||
import { makeExecutorErrorResult } from "../utils/error.ts";
|
||||
|
||||
const DESIGNER_WEB_BASE_URL =
|
||||
"https://designerapp.officeapps.live.com/designerapp/DallE.ashx?action=GetDallEImagesCogSci";
|
||||
|
||||
export class MicrosoftDesignerWebExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("microsoft-designer-web", { id: "microsoft-designer-web", baseUrl: DESIGNER_WEB_BASE_URL });
|
||||
}
|
||||
|
||||
async execute(_input: ExecuteInput) {
|
||||
return makeExecutorErrorResult(
|
||||
400,
|
||||
"microsoft-designer-web is an image-generation-only provider and does not support " +
|
||||
"chat completions. Use POST /v1/images/generations with model " +
|
||||
'"microsoft-designer-web/dall-e-3" instead.',
|
||||
_input.body,
|
||||
DESIGNER_WEB_BASE_URL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MicrosoftDesignerWebExecutor;
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
CHATGPT_WEB_IMAGE_ID_RE,
|
||||
} from "./imageGeneration/providers/chatgptWeb.ts";
|
||||
import { handleNvidiaNimImageGeneration } from "./imageGeneration/providers/nvidiaNim.ts";
|
||||
import { handleDesignerWebImageGeneration } from "./imageGeneration/providers/designerWeb.ts";
|
||||
import { handleMinimaxImageGeneration } from "./imageGeneration/providers/minimax.ts";
|
||||
|
||||
|
||||
@@ -462,6 +463,17 @@ export async function handleImageGeneration({
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "designer-web") {
|
||||
return handleDesignerWebImageGeneration({
|
||||
model,
|
||||
provider,
|
||||
providerConfig,
|
||||
body,
|
||||
credentials,
|
||||
log,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerConfig.format === "nanobanana") {
|
||||
return handleNanoBananaImageGeneration({
|
||||
model,
|
||||
|
||||
265
open-sse/handlers/imageGeneration/providers/designerWeb.ts
Normal file
265
open-sse/handlers/imageGeneration/providers/designerWeb.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/** 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 }
|
||||
): { ok: true; config: DesignerWebRequestConfig } | { ok: false; status: number; error: string } {
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
if (!prompt) {
|
||||
return {
|
||||
ok: false,
|
||||
status: 400,
|
||||
error: "Prompt is required for Microsoft Designer image generation",
|
||||
};
|
||||
}
|
||||
|
||||
const accessToken = credentials?.apiKey || credentials?.accessToken;
|
||||
if (!accessToken) {
|
||||
return { ok: false, 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 {
|
||||
ok: true,
|
||||
config: {
|
||||
prompt,
|
||||
accessToken,
|
||||
headers: buildDesignerWebHeaders({ accessToken }),
|
||||
formBody: buildDesignerWebFormBody(prompt, body.size),
|
||||
timeoutMs,
|
||||
pollIntervalMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type DesignerWebStepResult =
|
||||
| { done: false; waitMs: number }
|
||||
| { done: true; success: true; imageUrls: string[] }
|
||||
| { done: true; success: false; status: number; error: string };
|
||||
|
||||
/** 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 { done: true, success: false, status: resp.status, error: sanitizeErrorMessage(await resp.text()) };
|
||||
}
|
||||
|
||||
const parsed = parseDesignerWebResponse(await resp.json());
|
||||
|
||||
if (parsed.status === "ready") {
|
||||
return { done: true, success: true, imageUrls: parsed.imageUrls };
|
||||
}
|
||||
if (parsed.status === "empty") {
|
||||
return {
|
||||
done: true,
|
||||
success: false,
|
||||
status: 502,
|
||||
error: "Microsoft Designer response did not contain image data or polling metadata",
|
||||
};
|
||||
}
|
||||
return { done: false, 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<DesignerWebStepResult | { done: true; success: false; status: 504; error: string }> {
|
||||
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.done) return step;
|
||||
log?.info?.("IMAGE", `designer-web pending, poll #${attempt} in ${step.waitMs}ms`);
|
||||
await new Promise((resolve) => setTimeout(resolve, step.waitMs));
|
||||
}
|
||||
|
||||
return {
|
||||
done: true,
|
||||
success: false,
|
||||
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.ok) {
|
||||
return saveImageErrorResult({ provider, model, status: resolved.status, startTime, error: resolved.error });
|
||||
}
|
||||
|
||||
try {
|
||||
const outcome = await runDesignerWebPollLoop(providerConfig.baseUrl, resolved.config, fetchImpl, log);
|
||||
if (outcome.success) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,14 @@ const EMBEDDED_DEFAULTS = {
|
||||
],
|
||||
// Trae Cloud IDE — public oauth client id
|
||||
trae_id: [10, 3, 95, 6, 10, 22, 66, 3, 11, 90, 72, 31, 91, 2],
|
||||
// Microsoft Designer web app — public ClientId header sent by the
|
||||
// designer.microsoft.com frontend to designerapp.officeapps.live.com
|
||||
// (not a secret — every browser session sends the same fixed value;
|
||||
// reverse-engineered from the g4f MicrosoftDesigner provider reference).
|
||||
microsoft_designer_client_id: [
|
||||
13, 88, 13, 91, 68, 89, 65, 21, 72, 26, 21, 76, 0, 65, 93, 2, 26, 23, 28, 87, 14, 87, 8, 95, 12,
|
||||
17, 70, 6, 24, 66, 17, 1, 10, 95, 81, 28,
|
||||
],
|
||||
// Microsoft Edge Read Aloud (EdgeTTS) — public "trusted client token" used to
|
||||
// derive the Sec-MS-GEC anti-abuse header. Hardcoded in every known Edge
|
||||
// browser build and every open-source edge-tts reimplementation (e.g.
|
||||
|
||||
@@ -129,6 +129,19 @@ export const WEB_COOKIE_PROVIDERS = {
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"microsoft-designer-web": {
|
||||
id: "microsoft-designer-web",
|
||||
alias: "msdesigner",
|
||||
name: "Microsoft Designer (Image Generation)",
|
||||
icon: "auto_awesome",
|
||||
color: "#0078D4",
|
||||
textIcon: "MSD",
|
||||
website: "https://designer.microsoft.com",
|
||||
authHint:
|
||||
"Sign in at designer.microsoft.com, then open DevTools → Network, generate an image, and find the request to DallE.ashx?action=GetDallEImagesCogSci. Copy the value of its Authorization: Bearer header (the access_token — no 'Bearer ' prefix). The token is short-lived; this is an unofficial, reverse-engineered integration.",
|
||||
subscriptionRisk: true,
|
||||
riskNoticeVariant: "webCookie",
|
||||
},
|
||||
"t3-web": {
|
||||
id: "t3-web",
|
||||
alias: "t3chat",
|
||||
|
||||
@@ -109,6 +109,13 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: ["token", "access_token", "accessToken"],
|
||||
},
|
||||
"microsoft-designer-web": {
|
||||
kind: "token",
|
||||
credentialName: "access_token",
|
||||
placeholder: "access_token=... (Authorization: Bearer header from the DallE.ashx request)",
|
||||
acceptsFullCookieHeader: false,
|
||||
storageKeys: ["token", "access_token", "accessToken"],
|
||||
},
|
||||
"copilot-m365-web": {
|
||||
kind: "token",
|
||||
credentialName: "access_token + chathubPath",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* The duckduckgo-web executor was the first known case. To prevent any
|
||||
* future executor from regressing on the same contract, this sweep test
|
||||
* imports every executor in `WEB_COOKIE_PROVIDERS` + `NOAUTH_PROVIDERS`
|
||||
* (20 web-cookie + 2 noauth = 22 total), calls `execute()` with a minimal
|
||||
* (26 web-cookie + 2 noauth = 28 total), calls `execute()` with a minimal
|
||||
* but valid input, and asserts the wrapper shape. Tests use the
|
||||
* pre-aborted signal path or empty-creds path so no real upstream call
|
||||
* is needed.
|
||||
@@ -116,7 +116,7 @@ function assertExecutorWrapperShape(
|
||||
}
|
||||
|
||||
describe("web-cookie + noauth executor wrapper contract sweep", () => {
|
||||
describe("WEB_COOKIE_PROVIDERS (20)", () => {
|
||||
describe("WEB_COOKIE_PROVIDERS (26)", () => {
|
||||
for (const providerId of WEB_COOKIE_IDS) {
|
||||
it(`${providerId} executor returns wrapper shape`, async () => {
|
||||
const executor = getExecutor(providerId);
|
||||
|
||||
216
tests/unit/microsoft-designer-web-6672.test.ts
Normal file
216
tests/unit/microsoft-designer-web-6672.test.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
|
||||
import {
|
||||
buildDesignerWebHeaders,
|
||||
buildDesignerWebFormBody,
|
||||
mapDesignerWebImageSize,
|
||||
parseDesignerWebResponse,
|
||||
handleDesignerWebImageGeneration,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/designerWeb.ts";
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
// --- Registry entries -------------------------------------------------
|
||||
|
||||
test("microsoft-designer-web is registered in WEB_COOKIE_PROVIDERS with a webCookie risk notice", () => {
|
||||
const entry = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
|
||||
assert.ok(entry, "microsoft-designer-web must exist in WEB_COOKIE_PROVIDERS");
|
||||
assert.equal(entry.id, "microsoft-designer-web");
|
||||
assert.equal(entry.subscriptionRisk, true);
|
||||
assert.equal(entry.riskNoticeVariant, "webCookie");
|
||||
assert.match(entry.website, /designer\.microsoft\.com/);
|
||||
});
|
||||
|
||||
test("microsoft-designer-web is registered in IMAGE_PROVIDERS with the designer-web format", () => {
|
||||
const entry = (IMAGE_PROVIDERS as Record<string, unknown>)["microsoft-designer-web"];
|
||||
assert.ok(entry, "microsoft-designer-web must exist in IMAGE_PROVIDERS");
|
||||
assert.equal(entry.format, "designer-web");
|
||||
assert.match(entry.baseUrl, /designerapp\.officeapps\.live\.com/);
|
||||
assert.ok(Array.isArray(entry.models) && entry.models.length > 0);
|
||||
});
|
||||
|
||||
// --- Public credential (Hard Rule #11) ---------------------------------
|
||||
|
||||
test("microsoft_designer_client_id embedded default decodes to the public Designer ClientId", () => {
|
||||
assert.equal(resolvePublicCred("microsoft_designer_client_id"), "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
|
||||
});
|
||||
|
||||
test("designerWeb.ts never embeds the raw ClientId literal (Hard Rule #11)", () => {
|
||||
const src = fs.readFileSync(
|
||||
path.join(repoRoot, "open-sse/handlers/imageGeneration/providers/designerWeb.ts"),
|
||||
"utf8"
|
||||
) as string;
|
||||
assert.ok(
|
||||
!src.includes("b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621"),
|
||||
"designerWeb.ts must resolve the ClientId via resolvePublicCred(), not a string literal"
|
||||
);
|
||||
assert.ok(
|
||||
src.includes('resolvePublicCred("microsoft_designer_client_id")'),
|
||||
"designerWeb.ts must call resolvePublicCred for the Designer ClientId"
|
||||
);
|
||||
});
|
||||
|
||||
// --- Pure helpers --------------------------------------------------------
|
||||
|
||||
test("mapDesignerWebImageSize buckets sizes into square/landscape/portrait", () => {
|
||||
assert.equal(mapDesignerWebImageSize("1024x1024"), "1_1");
|
||||
assert.equal(mapDesignerWebImageSize("1792x1024"), "16_9");
|
||||
assert.equal(mapDesignerWebImageSize("1024x1792"), "9_16");
|
||||
assert.equal(mapDesignerWebImageSize(undefined), "1_1");
|
||||
assert.equal(mapDesignerWebImageSize("garbage"), "1_1");
|
||||
});
|
||||
|
||||
test("buildDesignerWebHeaders sets Bearer auth + the public ClientId + per-request SessionId/UserId", () => {
|
||||
const headers = buildDesignerWebHeaders({ accessToken: "tok-123" });
|
||||
assert.equal(headers.Authorization, "Bearer tok-123");
|
||||
assert.equal(headers.ClientId, "b5c2664a-7e9b-4a7a-8c9a-cd2c52dcf621");
|
||||
assert.ok(headers.SessionId && headers.SessionId.length > 0);
|
||||
assert.ok(headers.UserId && headers.UserId.length > 0);
|
||||
assert.equal(headers["Content-Type"], "application/x-www-form-urlencoded");
|
||||
});
|
||||
|
||||
test("buildDesignerWebFormBody encodes prompt, mapped size, fixed batch size, and a seed", () => {
|
||||
const form = buildDesignerWebFormBody("a cat astronaut", "1792x1024");
|
||||
assert.equal(form.get("dalle-caption"), "a cat astronaut");
|
||||
assert.equal(form.get("dalle-image-size"), "16_9");
|
||||
assert.equal(form.get("dalle-batch-size"), "4");
|
||||
assert.ok(Number(form.get("dalle-seed")) >= 0);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: ready state extracts thumbnail image URLs", () => {
|
||||
const parsed = parseDesignerWebResponse({
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/a.png" }, { ImageUrl: "https://example.com/b.png" }],
|
||||
});
|
||||
assert.equal(parsed.status, "ready");
|
||||
assert.deepEqual(parsed.imageUrls, ["https://example.com/a.png", "https://example.com/b.png"]);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: pending state surfaces the polling interval", () => {
|
||||
const parsed = parseDesignerWebResponse({
|
||||
polling_response: { polling_meta_data: { poll_interval: 1500 } },
|
||||
});
|
||||
assert.equal(parsed.status, "pending");
|
||||
assert.equal(parsed.pollIntervalMs, 1500);
|
||||
assert.deepEqual(parsed.imageUrls, []);
|
||||
});
|
||||
|
||||
test("parseDesignerWebResponse: unrecognized shape is 'empty'", () => {
|
||||
const parsed = parseDesignerWebResponse({ unexpected: true });
|
||||
assert.equal(parsed.status, "empty");
|
||||
});
|
||||
|
||||
// --- Handler (mocked fetch — no live Designer session required) ---------
|
||||
|
||||
function jsonResponse(status: number, body: unknown) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
test("handleDesignerWebImageGeneration returns 400 when prompt is missing", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: {},
|
||||
credentials: { apiKey: "tok" },
|
||||
fetchImpl: async () => jsonResponse(200, {}),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration returns 401 when access_token is missing", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat" },
|
||||
credentials: {},
|
||||
fetchImpl: async () => jsonResponse(200, {}),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration succeeds immediately when the first response is already ready", async () => {
|
||||
let calls = 0;
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", size: "1024x1024" },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
return jsonResponse(200, {
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/ready.png" }],
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].url, "https://example.com/ready.png");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration polls until ready, bounded by poll_interval_ms/timeout_ms", async () => {
|
||||
let calls = 0;
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 5000, poll_interval_ms: 1 },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () => {
|
||||
calls += 1;
|
||||
if (calls < 3) {
|
||||
return jsonResponse(200, {
|
||||
polling_response: { polling_meta_data: { poll_interval: 1 } },
|
||||
});
|
||||
}
|
||||
return jsonResponse(200, {
|
||||
image_urls_thumbnail: [{ ImageUrl: "https://example.com/final.png" }],
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(calls, 3);
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].url, "https://example.com/final.png");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration surfaces a sanitized error on a non-OK upstream response", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut" },
|
||||
credentials: { apiKey: "expired-token" },
|
||||
fetchImpl: async () => jsonResponse(401, { error: "invalid_token" }),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.ok(!String(result.error).includes(" at "), "error must not leak a stack trace");
|
||||
});
|
||||
|
||||
test("handleDesignerWebImageGeneration times out cleanly when the upstream never becomes ready", async () => {
|
||||
const result = await handleDesignerWebImageGeneration({
|
||||
model: "dall-e-3",
|
||||
provider: "microsoft-designer-web",
|
||||
providerConfig: { baseUrl: "https://designerapp.officeapps.live.com/designerapp/DallE.ashx" },
|
||||
body: { prompt: "a cat astronaut", timeout_ms: 5, poll_interval_ms: 1 },
|
||||
credentials: { apiKey: "tok-abc" },
|
||||
fetchImpl: async () =>
|
||||
jsonResponse(200, { polling_response: { polling_meta_data: { poll_interval: 1 } } }),
|
||||
});
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 504);
|
||||
});
|
||||
Reference in New Issue
Block a user