fix(sse): retire Microsoft Designer Web runtime (#11720)

Merged via /merge-batch (v3.8.51 provenance sweep). Boarded and validated together with the batch's other retirement PRs in a combined worktree — full gate suite green. This PR's own conflicts (against #11711's EdgeTTS retirement, both touching test-masking-allowlist.json and the "Image / video / audio generation" README bullet) were reconciled additively/subtractively (both retirements now correctly reflected), re-validated with this PR's own 62 focused tests, and pushed before merge. Thank you.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-28 04:49:02 -03:00
committed by GitHub
parent dd8575eb2c
commit 890cbfbfde
47 changed files with 1032 additions and 1238 deletions

View File

@@ -8,8 +8,8 @@ import { getWebSessionCredentialRequirement } from "@/shared/providers/webSessio
* `validateWebCookieProvider` probe (`src/lib/providers/validation/webCookie.ts`).
*
* `WEB_SESSION_CREDENTIAL_REQUIREMENTS` currently marks more providers as `kind: "token"`
* than have a matching validator (e.g. hailuo-web, microsoft-designer-web, t3-chat-web,
* promptql). Those fall through to `validateWebCookieProvider`'s generic probe, which
* than have a matching validator (e.g. hailuo-web, t3-chat-web, promptql). Those fall
* through to `validateWebCookieProvider`'s generic probe, which
* sends the stored credential as a `Cookie` header and treats most non-401/403 responses
* as valid — the wrong wire format for a token-authenticated provider, so an invalid
* token can be reported as a healthy connection. Keep this set in sync with
@@ -30,7 +30,6 @@ export function shouldUseApiKeyConnectionTest(authType: unknown, providerId: unk
if (authType !== "cookie") return false;
if (getWebSessionCredentialRequirement(providerId)?.kind !== "token") return false;
return (
typeof providerId === "string" &&
TOKEN_AWARE_VALIDATED_WEB_SESSION_PROVIDERS.has(providerId)
typeof providerId === "string" && TOKEN_AWARE_VALIDATED_WEB_SESSION_PROVIDERS.has(providerId)
);
}

View File

@@ -30,6 +30,7 @@ import {
extractImageEditInputFromJson,
validateCodexImageEditReferences,
} from "@/lib/images/imageRouteModel";
import { isMicrosoftDesignerWebProviderRetiredError } from "@/shared/constants/designerWebRetirement";
import { resolveProxyForConnection } from "@/lib/localDb";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import { isCodexFreePlan } from "@omniroute/open-sse/executors/codex/tools.ts";
@@ -363,7 +364,15 @@ async function postHandler(request: Request, _context?: unknown) {
// Resolve combo/alias, custom-provider prefix, and built-in ids consistently with
// /v1/images/generations (#3215).
const resolvedModel = await resolveImageRouteModel(fullModel);
let resolvedModel: string;
try {
resolvedModel = await resolveImageRouteModel(fullModel);
} catch (error) {
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
return errorResponse(HTTP_STATUS.GONE, error.message);
}
throw error;
}
const parsed = parseImageModel(resolvedModel);
const providerConfig = parsed.provider ? getImageProvider(parsed.provider) : null;
// Firefly nano/gpt-image accept multiple reference blobs; other non-Codex stay at 1.

View File

@@ -23,6 +23,11 @@ import { getComboByName } from "@/lib/db/combos";
import { getAllCustomModels } from "@/lib/db/models";
import { resolveProxyForConnection } from "@/lib/db/settings";
import { resolveImageRouteModel } from "@/lib/images/imageRouteModel";
import {
isMicrosoftDesignerWebProviderRetiredError,
isMicrosoftDesignerWebRetiredProviderId,
MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE,
} from "@/shared/constants/designerWebRetirement";
import {
resolveLocalSyncedEndpointRoute,
type LocalSyncedEndpointRoute,
@@ -122,21 +127,20 @@ async function postHandler(request, context) {
const policy = await enforceApiKeyPolicy(request, body.model);
if (policy.rejection) return policy.rejection;
const modelPrefix = body.model.includes("/")
? body.model.slice(0, body.model.indexOf("/"))
: body.model;
if (isMicrosoftDesignerWebRetiredProviderId(modelPrefix)) {
return errorResponse(HTTP_STATUS.GONE, MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE);
}
// #9239: Detect combo name and divert to full image combo execution.
// Checks before resolveImageRouteModel so we skip single-target flattening.
if (body.model && typeof body.model === "string" && !body.model.includes("/")) {
const combo = await getComboByName(body.model as string);
if (combo) {
const { executeImageCombo } = await import(
"@omniroute/open-sse/services/imageCombo"
);
return executeImageCombo(
body.model as string,
body,
{ request, policy },
startTime,
log
);
const { executeImageCombo } = await import("@omniroute/open-sse/services/imageCombo");
return executeImageCombo(body.model as string, body, { request, policy }, startTime, log);
}
}
@@ -144,7 +148,14 @@ async function postHandler(request, context) {
// model (`myImg/gpt-image-2`) to its internal `<nodeId>/<model>` form so the
// custom-model lookup and handler's resolvedProvider extraction resolve correctly.
// Built-in and already-internal ids pass through unchanged. Shared with /images/edits.
body.model = await resolveImageRouteModel(body.model);
try {
body.model = await resolveImageRouteModel(body.model);
} catch (error) {
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
return errorResponse(HTTP_STATUS.GONE, error.message);
}
throw error;
}
// Parse model to get provider
let { provider, model: requestedModel } = parseImageModel(body.model);
@@ -246,7 +257,8 @@ async function postHandler(request, context) {
provider,
null,
syncedEndpointRoute?.connectionIds ?? null,
requestedModel );
requestedModel
);
if (!credentials) {
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
@@ -346,7 +358,10 @@ async function postHandler(request, context) {
});
}
const errorPayload = toJsonErrorPayload((result as any).error, "Image generation provider error") as {
const errorPayload = toJsonErrorPayload(
(result as any).error,
"Image generation provider error"
) as {
error?: { message?: string };
};
const message =

View File

@@ -0,0 +1,122 @@
-- Permanently retire the Microsoft Designer Web reverse-engineered integration.
-- Keep connection rows, encrypted credentials, allowlists, and historical usage intact.
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
last_error = 'Provider retired from OmniRoute runtime.',
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
last_error_type = 'provider_retired',
last_error_source = 'migration:retire-microsoft-designer-web'
WHERE lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner');
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
end_reason = 'AUTHORIZATION_CHANGED'
WHERE state = 'ACTIVE'
AND (
lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
OR connection_id IN (
SELECT id
FROM provider_connections
WHERE lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
)
);
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_provider_insert
AFTER INSERT ON provider_connections
WHEN lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
BEGIN
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
last_error = 'Provider retired from OmniRoute runtime.',
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
last_error_type = 'provider_retired',
last_error_source = 'migration:retire-microsoft-designer-web'
WHERE id = NEW.id;
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
end_reason = 'AUTHORIZATION_CHANGED'
WHERE connection_id = NEW.id AND state = 'ACTIVE';
END;
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_provider_update
AFTER UPDATE OF provider, is_active, test_status, last_error, last_error_type, last_error_source
ON provider_connections
WHEN lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
AND (
COALESCE(NEW.is_active, 0) <> 0
OR COALESCE(NEW.test_status, '') <> 'unavailable'
OR COALESCE(NEW.last_error, '') <> 'Provider retired from OmniRoute runtime.'
OR COALESCE(NEW.last_error_type, '') <> 'provider_retired'
OR COALESCE(NEW.last_error_source, '') <> 'migration:retire-microsoft-designer-web'
)
BEGIN
UPDATE provider_connections
SET is_active = 0,
test_status = 'unavailable',
last_error = 'Provider retired from OmniRoute runtime.',
last_error_at = COALESCE(last_error_at, CURRENT_TIMESTAMP),
last_error_type = 'provider_retired',
last_error_source = 'migration:retire-microsoft-designer-web'
WHERE id = NEW.id;
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
end_reason = 'AUTHORIZATION_CHANGED'
WHERE connection_id = NEW.id AND state = 'ACTIVE';
END;
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_lease_insert
AFTER INSERT ON exclusive_connection_leases
WHEN NEW.state = 'ACTIVE'
AND (
lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
OR EXISTS (
SELECT 1
FROM provider_connections
WHERE id = NEW.connection_id
AND lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
)
)
BEGIN
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
end_reason = 'AUTHORIZATION_CHANGED'
WHERE id = NEW.id AND state = 'ACTIVE';
END;
CREATE TRIGGER IF NOT EXISTS trg_retire_microsoft_designer_web_lease_update
AFTER UPDATE OF provider, connection_id, state ON exclusive_connection_leases
WHEN NEW.state = 'ACTIVE'
AND (
lower(trim(NEW.provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
OR EXISTS (
SELECT 1
FROM provider_connections
WHERE id = NEW.connection_id
AND lower(trim(provider, ' ' || char(9) || char(10) || char(11) || char(12) || char(13)))
IN ('microsoft-designer-web', 'msdesigner')
)
)
BEGIN
UPDATE exclusive_connection_leases
SET state = 'INVALIDATED',
ended_at = COALESCE(ended_at, CURRENT_TIMESTAMP),
end_reason = 'AUTHORIZATION_CHANGED'
WHERE id = NEW.id AND state = 'ACTIVE';
END;

View File

@@ -34,6 +34,7 @@ import {
isMatchingOauthIdentity,
} from "./webSessionDedup";
import { pickCodexConnectionForUser } from "@/lib/oauth/utils/codexConnectionSelection";
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
import { reconcileCodexUsageHistory } from "./providers/usageIdentityReconciliation";
/**
@@ -595,6 +596,10 @@ export async function createProviderConnection(data: JsonRecord) {
_updateConnectionRow(db, existingId, encryptConnectionFields(persistence));
})();
backupDbFile("pre-write");
if (isMicrosoftDesignerWebRetiredProviderId(merged.provider)) {
invalidateDbCache("connections");
return getProviderConnectionById(existingId);
}
return withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(merged), merged),
@@ -722,6 +727,10 @@ export async function createProviderConnection(data: JsonRecord) {
backupDbFile("pre-write");
invalidateDbCache("connections"); // Bust connections read cache
if (isMicrosoftDesignerWebRetiredProviderId(data.provider)) {
return getProviderConnectionById(String(connection.id));
}
return withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(connection), connection),
@@ -967,6 +976,10 @@ export async function updateProviderConnection(id: string, data: JsonRecord) {
reorderConnections(db, providerId);
}
if (isMicrosoftDesignerWebRetiredProviderId(merged.provider)) {
return getProviderConnectionById(id);
}
return withNullableRateLimitOverrides(
withNullableQuotaWindowThresholds(
withNullableMaxConcurrent(cleanNulls(merged), merged),

View File

@@ -19,6 +19,7 @@ import { resolveComboTargets } from "@omniroute/open-sse/services/combo.ts";
import { getComboByName, getCombos } from "@/lib/db/combos";
import { getCachedProviderNodes } from "@/lib/localDb";
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
/**
* Rewrite a `prefix/model` custom image model to its internal `<nodeId>/<model>` form.
@@ -32,6 +33,7 @@ export async function resolveImageModelPrefix(modelStr: string): Promise<string>
if (slash <= 0) return modelStr;
const prefixPart = modelStr.slice(0, slash);
assertMicrosoftDesignerWebProviderAvailable(prefixPart);
const rest = modelStr.slice(slash + 1);
if (!rest) return modelStr;
@@ -75,6 +77,8 @@ export async function resolveSingleImageComboTarget(name: string): Promise<strin
*/
export async function resolveImageRouteModel(modelStr: string): Promise<string> {
if (typeof modelStr !== "string" || !modelStr.trim()) return modelStr;
const slash = modelStr.indexOf("/");
assertMicrosoftDesignerWebProviderAvailable(slash > 0 ? modelStr.slice(0, slash) : modelStr);
const parsedModel = parseImageModel(modelStr);
const hasSlash = modelStr.includes("/");

View File

@@ -0,0 +1,33 @@
export const RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS: ReadonlySet<string> = new Set([
"microsoft-designer-web",
"msdesigner",
]);
export const MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE =
"Provider has been retired from OmniRoute runtime.";
function normalizeProviderId(providerId: unknown): string {
return typeof providerId === "string" ? providerId.trim().toLowerCase() : "";
}
export function isMicrosoftDesignerWebRetiredProviderId(providerId: unknown): boolean {
return RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS.has(normalizeProviderId(providerId));
}
export function assertMicrosoftDesignerWebProviderAvailable(providerId: unknown): void {
if (!isMicrosoftDesignerWebRetiredProviderId(providerId)) return;
const error = new Error(MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE);
(error as Error & { status?: number }).status = 410;
throw error;
}
export function isMicrosoftDesignerWebProviderRetiredError(
error: unknown
): error is Error & { status: 410 } {
return (
error instanceof Error &&
(error as Error & { status?: number }).status === 410 &&
error.message === MICROSOFT_DESIGNER_WEB_RETIRED_MESSAGE
);
}

View File

@@ -156,19 +156,6 @@ 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",

View File

@@ -11,15 +11,21 @@
// provider (tokenrouter bug: "No active credentials for provider:
// tokenrouter" despite a fully configured compatible node).
//
// Semantics (mirror the original inline runtime guard exactly):
// - REGISTRY entry ids + aliases only. Manual alias ids outside REGISTRY
// (xiaomi/llamacpp/aq) do NOT intercept nodes at runtime and are therefore
// deliberately NOT reserved — including them would cause false-positive
// rejections.
// - Case-sensitive: mixed-case input like "TokenRouter" does not collide with
// the runtime lookup (`Set.has` is exact-match), so it stays allowed.
// Semantics:
// - Live REGISTRY entry ids + aliases, plus exact retired provider ids that
// must remain unavailable after their registry entries are removed. Manual
// aliases outside REGISTRY (xiaomi/llamacpp/aq) do NOT intercept nodes at
// runtime and are therefore deliberately NOT reserved — including them would
// cause false-positive rejections.
// - Live REGISTRY entries remain case-sensitive. Retired ids use their retirement
// normalizer (trim + lowercase), so casing cannot revive a removed provider.
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import {
isMicrosoftDesignerWebRetiredProviderId,
RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS,
} from "@/shared/constants/designerWebRetirement";
let _reserved: Set<string> | null = null;
function buildReservedProviderPrefixes(): Set<string> {
@@ -29,13 +35,16 @@ function buildReservedProviderPrefixes(): Set<string> {
if (entry?.id) reserved.add(entry.id);
if (entry?.alias) reserved.add(entry.alias);
}
for (const providerId of RETIRED_MICROSOFT_DESIGNER_WEB_PROVIDER_IDS) {
reserved.add(providerId);
}
_reserved = reserved;
return reserved;
}
/**
* All reserved provider prefixes (REGISTRY ids + aliases). Built lazily so the
* registry is only walked once per process.
* All canonical reserved provider prefixes (REGISTRY ids + aliases + retired ids).
* Built lazily so the registry is only walked once per process.
*/
export function getReservedProviderPrefixes(): ReadonlySet<string> {
return buildReservedProviderPrefixes();
@@ -58,7 +67,10 @@ export const RESERVED_PROVIDER_PREFIXES: ReadonlySet<string> = getReservedProvid
* reserved (mirrors the runtime guard's typeof check).
*/
export function isReservedProviderPrefix(value: unknown): boolean {
return typeof value === "string" && buildReservedProviderPrefixes().has(value);
return (
(typeof value === "string" && buildReservedProviderPrefixes().has(value)) ||
isMicrosoftDesignerWebRetiredProviderId(value)
);
}
/**

View File

@@ -163,13 +163,6 @@ 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",

View File

@@ -135,6 +135,7 @@ import { generateRequestId } from "../../shared/utils/requestId";
import { logAuditEvent } from "../../lib/compliance/index";
import { enforceApiKeyPolicy } from "../../shared/utils/apiKeyPolicy";
import { hasProviderQuotaBypassScope } from "../../shared/constants/apiKeyPolicyScopes";
import { isMicrosoftDesignerWebProviderRetiredError } from "../../shared/constants/designerWebRetirement";
import { cloneBoundedForLog } from "@omniroute/open-sse/utils/requestLogger.ts";
import { handleInternalUsageCommand } from "@/lib/usage/internalUsageCommand";
import {
@@ -948,7 +949,15 @@ async function handleChatImplementation(
// prefix may differ from the credential provider ID (e.g. model
// "xiaomi/mimo-v2-flash" resolves to provider "xiaomi" but the combo
// target specifies providerId: "opengate" for credential lookup).
const modelInfo = await getModelInfo(modelString);
let modelInfo;
try {
modelInfo = await getModelInfo(modelString);
} catch (error) {
// Persisted explicit combos may still reference the retired provider. Treat
// that target as unavailable so priority/fallback strategies can continue.
if (isMicrosoftDesignerWebProviderRetiredError(error)) return false;
throw error;
}
// Apply the same prefix-override guard as handleSingleModelChat:
// if providerId is just the prefix already in the model string, use
// the fully-resolved modelInfo.provider for a precise credential check.

View File

@@ -35,6 +35,7 @@ import {
} from "../../shared/utils/circuitBreaker";
import { classify429FromError, type FailureKind } from "../../shared/utils/classify429";
import { resolveUseUpstream429BreakerHints } from "../../shared/utils/providerHints";
import { isMicrosoftDesignerWebProviderRetiredError } from "../../shared/constants/designerWebRetirement";
import { logProxyEvent } from "../../lib/proxyLogger";
import { logTranslationEvent } from "../../lib/translatorEvents";
@@ -120,7 +121,15 @@ export async function resolveModelOrError(
endpointPath: string = "",
requestHeaders: Record<string, unknown> | null | undefined = null
) {
const modelInfo = await getModelInfo(modelStr);
let modelInfo;
try {
modelInfo = await getModelInfo(modelStr);
} catch (error) {
if (isMicrosoftDesignerWebProviderRetiredError(error)) {
return { error: errorResponse(HTTP_STATUS.GONE, error.message) };
}
throw error;
}
const sourceFormat = detectFormatFromEndpoint(body, endpointPath);
if (

View File

@@ -32,6 +32,7 @@ import {
import { buildJinaEnvCredentials } from "@/lib/providers/jina";
import { buildGeminiEnvCredentials } from "@/lib/providers/gemini";
import { toNumber } from "@/shared/utils/numeric";
import { isMicrosoftDesignerWebRetiredProviderId } from "@/shared/constants/designerWebRetirement";
import {
createLazyConnectionView,
toProviderConnection,
@@ -1283,6 +1284,12 @@ export async function getProviderCredentials(
requestedModel: string | null = null,
options: CredentialSelectionOptions = {}
) {
if (isMicrosoftDesignerWebRetiredProviderId(provider)) {
invalidateManagedLease(options, "AUTHORIZATION_CHANGED");
log.warn("AUTH", "Retired provider credential selection denied");
return null;
}
const selectionLock = options._leaseRetryWithLockHeld
? null
: createSelectionLock(getSelectionMutexKey(provider, options));

View File

@@ -21,6 +21,7 @@ import { getLearnedReasoningEffortForModel } from "@omniroute/open-sse/services/
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import { getRegisteredProviderEffortBaseModelId } from "@omniroute/open-sse/utils/registeredEffortVariants.ts";
import { getReservedProviderPrefixes } from "@/shared/constants/reservedProviderPrefixes";
import { assertMicrosoftDesignerWebProviderAvailable } from "@/shared/constants/designerWebRetirement";
export { parseModel, stripContextWindowSuffix };
@@ -425,6 +426,10 @@ export async function getModelInfo(modelStr) {
const parsed = parseModel(modelStr);
const { extendedContext } = parsed;
// Fail closed before a custom compatible node or stripModelPrefix can reinterpret
// an exact retired provider id/alias as an unrelated live provider.
assertMicrosoftDesignerWebProviderAvailable(parsed.providerAlias || parsed.provider);
const attachRuntimeModelMeta = async (info: any) => {
if (!info?.provider || !info?.model) return info;