Explorar o código

feat(ai): add Alibaba image generation

Aiden Cline hai 3 semanas
pai
achega
07ce99dc9e

+ 338 - 0
packages/ai/src/protocols/alibaba-images.ts

@@ -0,0 +1,338 @@
+import { Effect, Schema } from "effect"
+import { Headers, HttpClientRequest } from "effect/unstable/http"
+import {
+  GeneratedImage,
+  ImageModel,
+  ImageResponse,
+  type ImageModelDefaults,
+  type ImageRequest,
+  type ImageRoute,
+} from "../image"
+import { Auth, type Definition as AuthDefinition } from "../route/auth"
+import {
+  InvalidProviderOutputReason,
+  LLMError,
+  UnknownProviderReason,
+  Usage,
+  mergeHttpOptions,
+  mergeJsonRecords,
+} from "../schema"
+import { ProviderShared } from "./shared"
+
+const ADAPTER = "alibaba-images"
+export const DEFAULT_BASE_URL = "https://dashscope-intl.aliyuncs.com/api/v1"
+export const PATH = "/services/aigc/multimodal-generation/generation"
+
+export type Family = "qwen" | "wan"
+
+export interface QwenImageOptions extends Record<string, unknown> {
+  readonly negativePrompt?: string
+  readonly promptExtend?: boolean
+  readonly watermark?: boolean
+}
+
+export interface WanColor {
+  readonly hex: string
+  readonly ratio: string
+}
+
+export interface WanImageOptions extends Record<string, unknown> {
+  readonly resolution?: "1K" | "2K" | "4K"
+  readonly thinkingMode?: boolean
+  readonly colorPalette?: ReadonlyArray<WanColor>
+  readonly watermark?: boolean
+}
+
+export interface AlibabaImageOptions extends Record<string, unknown> {
+  readonly qwen?: QwenImageOptions
+  readonly wan?: WanImageOptions
+}
+
+declare module "../image" {
+  interface ImageProviderOptions {
+    readonly alibaba?: AlibabaImageOptions
+  }
+}
+
+const MessageInput = Schema.Struct({
+  messages: Schema.Tuple([
+    Schema.Struct({
+      role: Schema.tag("user"),
+      content: Schema.Tuple([Schema.Struct({ text: Schema.String })]),
+    }),
+  ]),
+})
+
+const QwenBody = Schema.Struct({
+  model: Schema.String,
+  input: MessageInput,
+  parameters: Schema.Struct({
+    size: Schema.optional(Schema.String),
+    n: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
+    negative_prompt: Schema.optional(Schema.String),
+    prompt_extend: Schema.optional(Schema.Boolean),
+    watermark: Schema.optional(Schema.Boolean),
+    seed: Schema.optional(Schema.Int),
+  }),
+})
+export type QwenBody = Schema.Schema.Type<typeof QwenBody>
+
+const WanBody = Schema.Struct({
+  model: Schema.String,
+  input: MessageInput,
+  parameters: Schema.Struct({
+    size: Schema.optional(Schema.String),
+    n: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
+    thinking_mode: Schema.optional(Schema.Boolean),
+    color_palette: Schema.optional(
+      Schema.Array(
+        Schema.Struct({
+          hex: Schema.String,
+          ratio: Schema.String,
+        }),
+      ),
+    ),
+    watermark: Schema.optional(Schema.Boolean),
+    seed: Schema.optional(Schema.Int),
+  }),
+})
+export type WanBody = Schema.Schema.Type<typeof WanBody>
+
+const AlibabaResponse = Schema.Struct({
+  output: Schema.optional(
+    Schema.Struct({
+      choices: Schema.Array(
+        Schema.Struct({
+          finish_reason: Schema.optional(Schema.String),
+          message: Schema.Struct({
+            role: Schema.optional(Schema.String),
+            content: Schema.Array(
+              Schema.Struct({
+                image: Schema.String,
+                type: Schema.optional(Schema.String),
+              }),
+            ),
+          }),
+        }),
+      ),
+      finished: Schema.optional(Schema.Boolean),
+    }),
+  ),
+  usage: Schema.optional(
+    Schema.Struct({
+      image_count: Schema.optional(Schema.Number),
+      input_tokens: Schema.optional(Schema.Number),
+      output_tokens: Schema.optional(Schema.Number),
+      total_tokens: Schema.optional(Schema.Number),
+      width: Schema.optional(Schema.Number),
+      height: Schema.optional(Schema.Number),
+      size: Schema.optional(Schema.String),
+    }),
+  ),
+  request_id: Schema.optional(Schema.String),
+  code: Schema.optional(Schema.String),
+  message: Schema.optional(Schema.String),
+})
+
+export interface ModelInput {
+  readonly id: string
+  readonly family: Family
+  readonly auth: AuthDefinition
+  readonly baseURL?: string
+  readonly headers?: Record<string, string>
+  readonly defaults?: ImageModelDefaults
+}
+
+const options = (request: ImageRequest): AlibabaImageOptions => ({
+  ...request.model.defaults?.providerOptions?.alibaba,
+  ...request.providerOptions?.alibaba,
+  qwen: {
+    ...(request.model.defaults?.providerOptions?.alibaba?.qwen as QwenImageOptions | undefined),
+    ...(request.providerOptions?.alibaba?.qwen as QwenImageOptions | undefined),
+  },
+  wan: {
+    ...(request.model.defaults?.providerOptions?.alibaba?.wan as WanImageOptions | undefined),
+    ...(request.providerOptions?.alibaba?.wan as WanImageOptions | undefined),
+  },
+})
+
+const messageInput = (request: ImageRequest) => ({
+  messages: [{ role: "user" as const, content: [{ text: request.prompt }] }] as const,
+})
+
+const qwenBody = (request: ImageRequest): QwenBody => {
+  const qwen = options(request).qwen
+  return {
+    model: request.model.id,
+    input: messageInput(request),
+    parameters: {
+      size: request.size === undefined ? undefined : `${request.size.width}*${request.size.height}`,
+      n: request.count,
+      negative_prompt: qwen?.negativePrompt,
+      prompt_extend: qwen?.promptExtend,
+      watermark: qwen?.watermark,
+      seed: request.seed,
+    },
+  }
+}
+
+const wanBody = (request: ImageRequest): WanBody => {
+  const wan = options(request).wan
+  return {
+    model: request.model.id,
+    input: messageInput(request),
+    parameters: {
+      size:
+        wan?.resolution ?? (request.size === undefined ? undefined : `${request.size.width}*${request.size.height}`),
+      n: request.count,
+      thinking_mode: wan?.thinkingMode,
+      color_palette: wan?.colorPalette === undefined ? undefined : [...wan.colorPalette],
+      watermark: wan?.watermark,
+      seed: request.seed,
+    },
+  }
+}
+
+const invalidOutput = (message: string, metadata?: Record<string, unknown>) =>
+  new LLMError({
+    module: ADAPTER,
+    method: "generate",
+    reason: new InvalidProviderOutputReason({
+      message,
+      route: ADAPTER,
+      providerMetadata: metadata === undefined ? undefined : { alibaba: metadata },
+    }),
+  })
+
+const providerError = (code: string | undefined, message: string | undefined, requestID: string | undefined) =>
+  new LLMError({
+    module: ADAPTER,
+    method: "generate",
+    reason: new UnknownProviderReason({
+      message: [code, message].filter(Boolean).join(": ") || "Alibaba image generation failed",
+      providerMetadata: { alibaba: { code, requestId: requestID } },
+    }),
+  })
+
+const applyQuery = (url: string, query: Record<string, string> | undefined) => {
+  if (!query) return url
+  const next = new URL(url)
+  Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value))
+  return next.toString()
+}
+
+const PROTOCOL_BODY_FIELDS = new Set(["model", "input", "parameters"])
+
+const bodyWithOverlay = Effect.fn("AlibabaImages.bodyWithOverlay")(function* (
+  imageBody: QwenBody | WanBody,
+  overlay: Record<string, unknown> | undefined,
+) {
+  if (!overlay) return imageBody
+  const reserved = Object.keys(overlay).filter((key) => PROTOCOL_BODY_FIELDS.has(key))
+  if (reserved.length > 0)
+    return yield* ProviderShared.invalidRequest(
+      `http.body cannot overlay protocol-owned field(s): ${reserved.join(", ")}`,
+    )
+  return mergeJsonRecords(imageBody, overlay) ?? imageBody
+})
+
+const expiration = (url: string) => {
+  if (!URL.canParse(url)) return undefined
+  const value = new URL(url).searchParams.get("Expires")
+  if (value === null || !Number.isFinite(Number(value))) return undefined
+  return new Date(Number(value) * 1000).toISOString()
+}
+
+export const model = (input: ModelInput) => {
+  const route: ImageRoute = {
+    id: `${ADAPTER}-${input.family}`,
+    generate: Effect.fn("AlibabaImages.generate")(function* (request: ImageRequest, execute) {
+      if (request.aspectRatio !== undefined)
+        return yield* ProviderShared.invalidRequest("Alibaba Images does not support the common aspectRatio option")
+      if (
+        input.family === "qwen" &&
+        (request.model.defaults?.providerOptions?.alibaba?.wan !== undefined ||
+          request.providerOptions?.alibaba?.wan !== undefined)
+      )
+        return yield* ProviderShared.invalidRequest("Qwen Image does not accept providerOptions.alibaba.wan")
+      if (
+        input.family === "wan" &&
+        (request.model.defaults?.providerOptions?.alibaba?.qwen !== undefined ||
+          request.providerOptions?.alibaba?.qwen !== undefined)
+      )
+        return yield* ProviderShared.invalidRequest("Wan Image does not accept providerOptions.alibaba.qwen")
+      if (input.family === "wan" && request.size !== undefined && options(request).wan?.resolution !== undefined)
+        return yield* ProviderShared.invalidRequest("Wan Image accepts either size or resolution, not both")
+
+      const requestBody = yield* ProviderShared.validateWith(
+        Schema.decodeUnknownEffect(input.family === "qwen" ? QwenBody : WanBody),
+      )(input.family === "qwen" ? qwenBody(request) : wanBody(request))
+      const http = mergeHttpOptions(request.model.defaults?.http, request.http)
+      const overlaidBody = yield* bodyWithOverlay(requestBody, http?.body)
+      const text = ProviderShared.encodeJson(overlaidBody)
+      const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${PATH}`, http?.query)
+      const headers = yield* Auth.toEffect(input.auth)({
+        request,
+        method: "POST",
+        url,
+        body: text,
+        headers: Headers.fromInput({ ...input.headers, ...http?.headers }),
+      })
+      const response = yield* execute(
+        HttpClientRequest.post(url).pipe(
+          HttpClientRequest.setHeaders(headers),
+          HttpClientRequest.bodyText(text, "application/json"),
+        ),
+      )
+      const payload = yield* response.json.pipe(
+        Effect.mapError(() => invalidOutput("Failed to read the Alibaba Images response")),
+      )
+      const decoded = yield* Schema.decodeUnknownEffect(AlibabaResponse)(payload).pipe(
+        Effect.mapError(() => invalidOutput("Alibaba Images returned an invalid response")),
+      )
+      if (decoded.code !== undefined || decoded.output === undefined)
+        return yield* providerError(decoded.code, decoded.message, decoded.request_id)
+      const urls = decoded.output.choices.flatMap((choice) => choice.message.content.map((content) => content.image))
+      if (urls.length === 0)
+        return yield* invalidOutput("Alibaba Images returned no images", { requestId: decoded.request_id })
+
+      return new ImageResponse({
+        images: urls.map(
+          (url) =>
+            new GeneratedImage({
+              mediaType: "image/png",
+              data: url,
+              providerMetadata: {
+                alibaba: {
+                  modelId: request.model.id,
+                  family: input.family,
+                  expiresAt: expiration(url),
+                },
+              },
+            }),
+        ),
+        usage:
+          decoded.usage === undefined
+            ? undefined
+            : new Usage({
+                inputTokens: decoded.usage.input_tokens,
+                outputTokens: decoded.usage.output_tokens,
+                totalTokens: decoded.usage.total_tokens,
+                providerMetadata: { alibaba: decoded.usage },
+              }),
+        providerMetadata: {
+          alibaba: {
+            requestId: decoded.request_id,
+            modelId: request.model.id,
+            family: input.family,
+          },
+        },
+      })
+    }),
+  }
+  return ImageModel.make({ id: input.id, provider: "alibaba", route, defaults: input.defaults })
+}
+
+export const AlibabaImages = {
+  model,
+} as const

+ 1 - 0
packages/ai/src/protocols/index.ts

@@ -1,4 +1,5 @@
 export * as AnthropicMessages from "./anthropic-messages"
+export * as AlibabaImages from "./alibaba-images"
 export * as BedrockConverse from "./bedrock-converse"
 export * as Gemini from "./gemini"
 export * as OpenAIChat from "./openai-chat"

+ 48 - 0
packages/ai/src/providers/alibaba.ts

@@ -0,0 +1,48 @@
+import type { ImageModel } from "../image"
+import { AlibabaImages, type AlibabaImageOptions, type Family } from "../protocols/alibaba-images"
+import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
+import { HttpOptions, ProviderID, mergeHttpOptions } from "../schema"
+
+export type { AlibabaImageOptions, QwenImageOptions, WanColor, WanImageOptions } from "../protocols/alibaba-images"
+
+export type AlibabaImageModelID = "qwen-image-2.0" | "qwen-image-2.0-pro" | "wan2.7-image" | "wan2.7-image-pro"
+
+export const id = ProviderID.make("alibaba")
+
+export type Config = ProviderAuthOption<"optional"> & {
+  readonly baseURL?: string
+  readonly headers?: Record<string, string>
+  readonly http?: HttpOptions.Input
+  readonly image?: {
+    readonly providerOptions?: AlibabaImageOptions
+  }
+}
+
+const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "DASHSCOPE_API_KEY")
+
+const family = (modelID: AlibabaImageModelID): Family => (modelID.startsWith("qwen-") ? "qwen" : "wan")
+
+export const configure = (input: Config = {}) => {
+  const image = (modelID: AlibabaImageModelID): ImageModel =>
+    AlibabaImages.model({
+      id: modelID,
+      family: family(modelID),
+      auth: auth(input),
+      baseURL: input.baseURL,
+      headers: input.headers,
+      defaults: {
+        providerOptions:
+          input.image?.providerOptions === undefined ? undefined : { alibaba: { ...input.image.providerOptions } },
+        http: mergeHttpOptions(input.http === undefined ? undefined : HttpOptions.make(input.http)),
+      },
+    })
+
+  return {
+    id,
+    image,
+    configure,
+  }
+}
+
+export const provider = configure()
+export const image = provider.image

+ 1 - 0
packages/ai/src/providers/index.ts

@@ -1,6 +1,7 @@
 export * as Anthropic from "./anthropic"
 export * as AnthropicCompatible from "./anthropic-compatible"
 export * as AmazonBedrock from "./amazon-bedrock"
+export * as Alibaba from "./alibaba"
 export * as Azure from "./azure"
 export * as Cloudflare from "./cloudflare"
 export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"

+ 28 - 0
packages/ai/test/fixtures/recordings/alibaba-images/generates-with-qwen-image-2-0.json

@@ -0,0 +1,28 @@
+{
+  "version": 1,
+  "metadata": {
+    "tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"],
+    "name": "alibaba-images/generates-with-qwen-image-2-0",
+    "recordedAt": "2026-07-19T16:05:19.642Z"
+  },
+  "interactions": [
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
+        "headers": {
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"qwen-image-2.0\",\"input\":{\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"A simple flat black circle centered on a plain white background.\"}]}]},\"parameters\":{\"size\":\"512*512\",\"prompt_extend\":false,\"watermark\":false}}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "application/json"
+        },
+        "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/7d/7b/20260720/991d10e3/fe425a64-dc6f-4c67-a4fa-a06810984ba9.png?Expires=1785082919&OSSAccessKeyId=fixture&Signature=fixture\"}],\"role\":\"assistant\"}}]},\"usage\":{\"height\":512,\"image_count\":1,\"width\":512},\"request_id\":\"484490f4-9881-90eb-8d52-d270d2395920\"}"
+      }
+    }
+  ]
+}

+ 28 - 0
packages/ai/test/fixtures/recordings/alibaba-images/generates-with-wan-2-7.json

@@ -0,0 +1,28 @@
+{
+  "version": 1,
+  "metadata": {
+    "tags": ["prefix:alibaba-images", "provider:alibaba", "protocol:alibaba-images"],
+    "name": "alibaba-images/generates-with-wan-2-7",
+    "recordedAt": "2026-07-19T16:05:23.617Z"
+  },
+  "interactions": [
+    {
+      "transport": "http",
+      "request": {
+        "method": "POST",
+        "url": "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
+        "headers": {
+          "content-type": "application/json"
+        },
+        "body": "{\"model\":\"wan2.7-image\",\"input\":{\"messages\":[{\"role\":\"user\",\"content\":[{\"text\":\"A simple flat black square centered on a plain white background.\"}]}]},\"parameters\":{\"size\":\"1K\",\"thinking_mode\":false,\"watermark\":false}}"
+      },
+      "response": {
+        "status": 200,
+        "headers": {
+          "content-type": "application/json"
+        },
+        "body": "{\"output\":{\"choices\":[{\"finish_reason\":\"stop\",\"message\":{\"content\":[{\"image\":\"https://dashscope-463f.oss-accelerate.aliyuncs.com/1d/ae/20260720/10739b93/1784477122_595381b5.png?Expires=1784563522&OSSAccessKeyId=fixture&Signature=fixture\",\"type\":\"image\"}],\"role\":\"assistant\"}}],\"finished\":true},\"usage\":{\"image_count\":1,\"input_tokens\":31,\"output_tokens\":2,\"size\":\"1024*1024\",\"total_tokens\":33},\"request_id\":\"ec5cacb5-c7b0-9e46-8459-49003a3816c3\"}"
+      }
+    }
+  ]
+}

+ 47 - 0
packages/ai/test/provider/alibaba-images.recorded.test.ts

@@ -0,0 +1,47 @@
+import { describe, expect } from "bun:test"
+import { Effect } from "effect"
+import { Image } from "../../src"
+import { Alibaba } from "../../src/providers"
+import { recordedTests } from "../recorded-test"
+
+const alibaba = Alibaba.configure({
+  apiKey: process.env.DASHSCOPE_API_KEY ?? "fixture",
+})
+
+const recorded = recordedTests({
+  prefix: "alibaba-images",
+  provider: "alibaba",
+  protocol: "alibaba-images",
+  requires: ["DASHSCOPE_API_KEY"],
+})
+
+describe("Alibaba Images recorded", () => {
+  recorded.effect("generates with Qwen Image 2.0", () =>
+    Effect.gen(function* () {
+      const response = yield* Image.generate({
+        model: alibaba.image("qwen-image-2.0"),
+        prompt: "A simple flat black circle centered on a plain white background.",
+        size: { width: 512, height: 512 },
+        providerOptions: { alibaba: { qwen: { promptExtend: false, watermark: false } } },
+      })
+
+      expect(response.images).toHaveLength(1)
+      expect(response.image?.mediaType).toBe("image/png")
+      expect(response.image?.data).toStartWith("https://")
+    }),
+  )
+
+  recorded.effect("generates with Wan 2.7", () =>
+    Effect.gen(function* () {
+      const response = yield* Image.generate({
+        model: alibaba.image("wan2.7-image"),
+        prompt: "A simple flat black square centered on a plain white background.",
+        providerOptions: { alibaba: { wan: { resolution: "1K", thinkingMode: false, watermark: false } } },
+      })
+
+      expect(response.images).toHaveLength(1)
+      expect(response.image?.mediaType).toBe("image/png")
+      expect(response.image?.data).toStartWith("https://")
+    }),
+  )
+})

+ 196 - 0
packages/ai/test/provider/alibaba-images.test.ts

@@ -0,0 +1,196 @@
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
+import { HttpClientRequest } from "effect/unstable/http"
+import { Image, ImageClient } from "../../src"
+import { Alibaba } from "../../src/providers"
+import { it } from "../lib/effect"
+import { dynamicResponse } from "../lib/http"
+
+const response = (family: "qwen" | "wan") => ({
+  output: {
+    choices: [
+      {
+        finish_reason: "stop",
+        message: {
+          role: "assistant",
+          content: [
+            {
+              image: "https://dashscope-result-intl.oss-cn-singapore.aliyuncs.com/result.png?Expires=1893456000",
+              ...(family === "wan" ? { type: "image" } : {}),
+            },
+          ],
+        },
+      },
+    ],
+    ...(family === "wan" ? { finished: true } : {}),
+  },
+  usage:
+    family === "wan"
+      ? { image_count: 1, input_tokens: 10, output_tokens: 2, total_tokens: 12, size: "2048*2048" }
+      : { image_count: 1, width: 1024, height: 768 },
+  request_id: `request-${family}`,
+})
+
+describe("Alibaba Images", () => {
+  it.effect("generates Qwen Image 2.0 through the international synchronous route", () =>
+    Effect.gen(function* () {
+      const result = yield* Image.generate({
+        model: Alibaba.configure({
+          apiKey: "test",
+          baseURL: "https://dashscope-intl.test/api/v1",
+          image: { providerOptions: { qwen: { promptExtend: true, watermark: false } } },
+          http: { headers: { "x-default": "yes" } },
+        }).image("qwen-image-2.0-pro"),
+        prompt: "A robot tending a rooftop garden",
+        count: 2,
+        size: { width: 1024, height: 768 },
+        seed: 42,
+        providerOptions: { alibaba: { qwen: { negativePrompt: "blurry" } } },
+        http: { headers: { "x-request": "yes" }, query: { trace: "1" }, body: { metadata: "test" } },
+      })
+
+      expect(result.image?.data).toContain("dashscope-result-intl")
+      expect(result.image?.providerMetadata).toEqual({
+        alibaba: {
+          modelId: "qwen-image-2.0-pro",
+          family: "qwen",
+          expiresAt: "2030-01-01T00:00:00.000Z",
+        },
+      })
+      expect(result.providerMetadata).toEqual({
+        alibaba: { requestId: "request-qwen", modelId: "qwen-image-2.0-pro", family: "qwen" },
+      })
+    }).pipe(
+      Effect.provide(
+        ImageClient.layer.pipe(
+          Layer.provide(
+            dynamicResponse((input) =>
+              Effect.gen(function* () {
+                const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+                expect(request.url).toBe(
+                  "https://dashscope-intl.test/api/v1/services/aigc/multimodal-generation/generation?trace=1",
+                )
+                expect(request.headers.get("authorization")).toBe("Bearer test")
+                expect(request.headers.get("x-default")).toBe("yes")
+                expect(request.headers.get("x-request")).toBe("yes")
+                expect(JSON.parse(input.text)).toEqual({
+                  model: "qwen-image-2.0-pro",
+                  input: {
+                    messages: [{ role: "user", content: [{ text: "A robot tending a rooftop garden" }] }],
+                  },
+                  parameters: {
+                    size: "1024*768",
+                    n: 2,
+                    negative_prompt: "blurry",
+                    prompt_extend: true,
+                    watermark: false,
+                    seed: 42,
+                  },
+                  metadata: "test",
+                })
+                return input.respond(JSON.stringify(response("qwen")), {
+                  headers: { "content-type": "application/json" },
+                })
+              }),
+            ),
+          ),
+        ),
+      ),
+    ),
+  )
+
+  it.effect("generates Wan 2.7 through the international synchronous route", () =>
+    Effect.gen(function* () {
+      const result = yield* Image.generate({
+        model: Alibaba.configure({ apiKey: "test", baseURL: "https://dashscope-intl.test/api/v1" }).image(
+          "wan2.7-image-pro",
+        ),
+        prompt: "A flower shop with a wooden door",
+        count: 1,
+        seed: 7,
+        providerOptions: {
+          alibaba: {
+            wan: {
+              resolution: "2K",
+              thinkingMode: true,
+              watermark: false,
+              colorPalette: [
+                { hex: "#112233", ratio: "60.00%" },
+                { hex: "#445566", ratio: "25.00%" },
+                { hex: "#778899", ratio: "15.00%" },
+              ],
+            },
+          },
+        },
+      })
+
+      expect(result.images).toHaveLength(1)
+      expect(result.image?.mediaType).toBe("image/png")
+      expect(result.usage?.totalTokens).toBe(12)
+      expect(result.usage?.providerMetadata).toEqual({ alibaba: response("wan").usage })
+    }).pipe(
+      Effect.provide(
+        ImageClient.layer.pipe(
+          Layer.provide(
+            dynamicResponse((input) =>
+              Effect.gen(function* () {
+                const request = yield* HttpClientRequest.toWeb(input.request).pipe(Effect.orDie)
+                expect(request.url).toBe(
+                  "https://dashscope-intl.test/api/v1/services/aigc/multimodal-generation/generation",
+                )
+                expect(JSON.parse(input.text)).toEqual({
+                  model: "wan2.7-image-pro",
+                  input: { messages: [{ role: "user", content: [{ text: "A flower shop with a wooden door" }] }] },
+                  parameters: {
+                    size: "2K",
+                    n: 1,
+                    thinking_mode: true,
+                    color_palette: [
+                      { hex: "#112233", ratio: "60.00%" },
+                      { hex: "#445566", ratio: "25.00%" },
+                      { hex: "#778899", ratio: "15.00%" },
+                    ],
+                    watermark: false,
+                    seed: 7,
+                  },
+                })
+                return input.respond(JSON.stringify(response("wan")), {
+                  headers: { "content-type": "application/json" },
+                })
+              }),
+            ),
+          ),
+        ),
+      ),
+    ),
+  )
+
+  it.effect("surfaces Alibaba error envelopes as typed provider errors", () =>
+    Image.generate({
+      model: Alibaba.configure({ apiKey: "test" }).image("qwen-image-2.0"),
+      prompt: "A robot",
+    }).pipe(
+      Effect.flip,
+      Effect.tap((error) =>
+        Effect.sync(() => {
+          expect(error.reason._tag).toBe("UnknownProvider")
+          expect(error.message).toContain("InvalidParameter: invalid prompt")
+        }),
+      ),
+      Effect.provide(
+        ImageClient.layer.pipe(
+          Layer.provide(
+            dynamicResponse((input) =>
+              Effect.succeed(
+                input.respond(
+                  JSON.stringify({ request_id: "request-error", code: "InvalidParameter", message: "invalid prompt" }),
+                  { headers: { "content-type": "application/json" } },
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    ),
+  )
+})