feat(ai): add Z.ai image generation (#37780)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
parent
4c9a4309ce
commit
fa4ec98e1d
8 changed files with 391 additions and 2 deletions
28
packages/ai/test/fixtures/recordings/zai-images/generates-an-image.json
vendored
Normal file
28
packages/ai/test/fixtures/recordings/zai-images/generates-an-image.json
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"version": 1,
|
||||
"metadata": {
|
||||
"tags": ["prefix:zai-images", "provider:zai", "protocol:zai-images"],
|
||||
"name": "zai-images/generates-an-image",
|
||||
"recordedAt": "2026-07-19T16:03:55.761Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
"transport": "http",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"url": "https://api.z.ai/api/paas/v4/images/generations",
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "{\"model\":\"cogview-4-250304\",\"prompt\":\"A simple flat red circle centered on a plain white background.\",\"size\":\"1024x1024\",\"quality\":\"standard\",\"user_id\":\"opencode-image-test\"}"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json; charset=UTF-8"
|
||||
},
|
||||
"body": "{\"created\":1784477028,\"data\":[{\"url\":\"https://mfile.z.ai/1784477035500-43574eab2b6e402da9063d6ac22dfefb.png?ufileattname=202607200003482062c3bba9b04f7d_watermark.png\"}],\"id\":\"202607200003482062c3bba9b04f7d\",\"request_id\":\"202607200003482062c3bba9b04f7d\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type ImageRequestFor,
|
||||
type ImageRoute,
|
||||
} from "../src"
|
||||
import { Google, OpenAI, XAI } from "../src/providers"
|
||||
import { Google, OpenAI, XAI, ZAI } from "../src/providers"
|
||||
|
||||
type GoogleLikeOptions = {
|
||||
readonly aspectRatio?: "1:1" | "16:9"
|
||||
|
|
@ -99,6 +99,20 @@ Image.generate({ model: xai, prompt: "A lighthouse", options: { n: "2" } })
|
|||
// @ts-expect-error Known xAI string options retain their value kind.
|
||||
Image.generate({ model: xai, prompt: "A lighthouse", options: { resolution: 2 } })
|
||||
|
||||
const zai = ZAI.configure({ apiKey: "test" }).image("any-model-id")
|
||||
// @ts-expect-error Image generation options are request-scoped, not provider configuration.
|
||||
ZAI.configure({ image: { options: { quality: "hd" } } })
|
||||
Image.generate({
|
||||
model: zai,
|
||||
prompt: "A lighthouse",
|
||||
options: { quality: "future-quality", userID: "user-123", future_option: true },
|
||||
})
|
||||
Image.generate({ model: zai, prompt: "A lighthouse", options: { user_id: "raw-user" } })
|
||||
// @ts-expect-error Known Z.ai string options retain their value kind.
|
||||
Image.generate({ model: zai, prompt: "A lighthouse", options: { quality: 1 } })
|
||||
// @ts-expect-error Known Z.ai user IDs retain their value kind.
|
||||
Image.generate({ model: zai, prompt: "A lighthouse", options: { userID: 1 } })
|
||||
|
||||
declare const generic: ImageModel<ImageOptions>
|
||||
Image.generate({ model: generic, prompt: "A lighthouse", options: { arbitrary: true } })
|
||||
|
||||
|
|
|
|||
32
packages/ai/test/provider/zai-images.recorded.test.ts
Normal file
32
packages/ai/test/provider/zai-images.recorded.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { Image } from "../../src"
|
||||
import { ZAI } from "../../src/providers"
|
||||
import { recordedTests } from "../recorded-test"
|
||||
|
||||
const model = ZAI.configure({ apiKey: process.env.ZAI_API_KEY ?? "fixture" }).image("cogview-4-250304")
|
||||
|
||||
const recorded = recordedTests({
|
||||
prefix: "zai-images",
|
||||
provider: "zai",
|
||||
protocol: "zai-images",
|
||||
requires: ["ZAI_API_KEY"],
|
||||
})
|
||||
|
||||
describe("Z.ai Images recorded", () => {
|
||||
recorded.effect("generates an image", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model,
|
||||
prompt: "A simple flat red circle centered on a plain white background.",
|
||||
options: { size: "1024x1024", quality: "standard", userID: "opencode-image-test" },
|
||||
})
|
||||
|
||||
expect(response.images).toHaveLength(1)
|
||||
expect(response.image?.mediaType).toBe("application/octet-stream")
|
||||
expect(response.image?.data).toBeString()
|
||||
expect(response.image?.data).toStartWith("https://")
|
||||
expect(response.providerMetadata?.zai).toBeDefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
130
packages/ai/test/provider/zai-images.test.ts
Normal file
130
packages/ai/test/provider/zai-images.test.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Image, ImageClient } from "../../src"
|
||||
import { ZAI } from "../../src/providers"
|
||||
import { it } from "../lib/effect"
|
||||
import { dynamicResponse, fixedResponse } from "../lib/http"
|
||||
|
||||
describe("Z.ai Images", () => {
|
||||
it.effect("generates through the Z.ai Images API", () =>
|
||||
Effect.gen(function* () {
|
||||
const response = yield* Image.generate({
|
||||
model: ZAI.configure({
|
||||
apiKey: "test",
|
||||
baseURL: "https://api.z.ai.test/api/paas/v4",
|
||||
headers: { "x-default": "yes" },
|
||||
http: { body: { configured: true, quality: "configured" }, query: { trace: "default" } },
|
||||
}).image("glm-image"),
|
||||
prompt: "A red circle on a white background",
|
||||
options: {
|
||||
quality: "hd",
|
||||
userID: "alias-user",
|
||||
user_id: "raw-user",
|
||||
future_option: true,
|
||||
},
|
||||
http: {
|
||||
headers: { "x-request": "yes" },
|
||||
query: { trace: "request" },
|
||||
body: { quality: "final", user_id: "final-user" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(response.images).toHaveLength(1)
|
||||
expect(response.image?.mediaType).toBe("application/octet-stream")
|
||||
expect(response.image?.data).toBe("https://cdn.z.ai/generated.png")
|
||||
expect(response.providerMetadata).toEqual({
|
||||
zai: {
|
||||
created: 1_760_335_349,
|
||||
id: "generation-1",
|
||||
requestID: "request-1",
|
||||
contentFilter: [{ role: "future-role", level: 4.5 }],
|
||||
},
|
||||
})
|
||||
}).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://api.z.ai.test/api/paas/v4/images/generations?trace=request")
|
||||
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: "glm-image",
|
||||
prompt: "A red circle on a white background",
|
||||
quality: "final",
|
||||
user_id: "final-user",
|
||||
future_option: true,
|
||||
configured: true,
|
||||
})
|
||||
return input.respond(
|
||||
JSON.stringify({
|
||||
created: 1_760_335_349,
|
||||
id: "generation-1",
|
||||
request_id: "request-1",
|
||||
data: [{ url: "https://cdn.z.ai/generated.png" }],
|
||||
content_filter: [{ role: "future-role", level: 4.5 }],
|
||||
}),
|
||||
{ headers: { "content-type": "application/json" } },
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("lets raw native options override aliases", () =>
|
||||
Image.generate({
|
||||
model: ZAI.configure({ apiKey: "test" }).image("model"),
|
||||
prompt: "test",
|
||||
options: { quality: "future-quality", userID: "x", user_id: "raw-user" },
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
dynamicResponse((input) => {
|
||||
expect(JSON.parse(input.text)).toMatchObject({ quality: "future-quality", user_id: "raw-user" })
|
||||
return Effect.succeed(
|
||||
input.respond(JSON.stringify({ data: [{ url: "https://example.test/image.jpg" }] }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("rejects invalid response structures", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = ZAI.configure({ apiKey: "test" }).image("model")
|
||||
const payloads = [
|
||||
{},
|
||||
{ data: [] },
|
||||
{ data: [{ b64_json: "image" }] },
|
||||
{ data: [{ url: 1 }] },
|
||||
{ data: [{ url: "https://example.test/image.jpg" }], content_filter: [{ role: 1, level: "high" }] },
|
||||
]
|
||||
|
||||
yield* Effect.forEach(payloads, (payload) =>
|
||||
Image.generate({ model, prompt: "test" }).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
fixedResponse(JSON.stringify(payload), { headers: { "content-type": "application/json" } }),
|
||||
),
|
||||
),
|
||||
),
|
||||
Effect.flip,
|
||||
Effect.tap((error) => Effect.sync(() => expect(error.reason._tag).toBe("InvalidProviderOutput"))),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue