feat(ai): add Z.ai image generation

This commit is contained in:
Aiden Cline
2026-07-19 16:05:45 +00:00
committed by 𝓛𝓲𝓽𝓽𝓵𝓮 𝓕𝓻𝓪𝓷𝓴
parent bef6cfbffe
commit efda817636
6 changed files with 257 additions and 19 deletions
+75 -19
View File
@@ -25,16 +25,24 @@ export interface OpenAIImageOptions {
readonly outputCompression?: number
}
export interface ZAIImageOptions {
readonly quality?: "hd" | "standard"
readonly userID?: string
}
export type ImageProtocol = "openai" | "zai"
const OpenAIImageBody = Schema.Struct({
model: Schema.String,
prompt: Schema.String,
n: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))),
size: Schema.optional(Schema.String),
quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high"])),
size: Schema.optional(OpenAIImage.Size),
quality: Schema.optional(Schema.Literals(["auto", "low", "medium", "high", "hd", "standard"])),
background: Schema.optional(Schema.Literals(["auto", "opaque", "transparent"])),
moderation: Schema.optional(Schema.Literals(["auto", "low"])),
output_format: Schema.optional(Schema.Literals(["png", "jpeg", "webp"])),
output_compression: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 }))),
user_id: Schema.optional(Schema.String),
})
export type OpenAIImageBody = Schema.Schema.Type<typeof OpenAIImageBody>
@@ -56,10 +64,22 @@ const OpenAIImageResponse = Schema.Struct({
output_tokens_details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
}),
),
created: Schema.optional(Schema.Int),
id: Schema.optional(Schema.String),
request_id: Schema.optional(Schema.String),
content_filter: Schema.optional(
Schema.Array(
Schema.Struct({
role: Schema.optional(Schema.String),
level: Schema.optional(Schema.Number),
}),
),
),
})
export interface ModelInput {
readonly id: string
readonly protocol?: ImageProtocol
readonly auth: AuthDefinition
readonly baseURL?: string
readonly headers?: Record<string, string>
@@ -71,7 +91,22 @@ const providerOptions = (request: ImageRequest): OpenAIImageOptions => ({
...request.providerOptions?.openai,
})
const body = (request: ImageRequest): OpenAIImageBody => {
const zaiOptions = (request: ImageRequest): ZAIImageOptions => ({
...request.model.defaults?.providerOptions?.zai,
...request.providerOptions?.zai,
})
const body = (request: ImageRequest, protocol: ImageProtocol): OpenAIImageBody => {
if (protocol === "zai") {
const options = zaiOptions(request)
return {
model: request.model.id,
prompt: request.prompt,
size: request.size === undefined ? undefined : `${request.size.width}x${request.size.height}`,
quality: options.quality,
user_id: options.userID,
}
}
const options = providerOptions(request)
return {
model: request.model.id,
@@ -86,11 +121,11 @@ const body = (request: ImageRequest): OpenAIImageBody => {
}
}
const invalidOutput = (message: string) =>
const invalidOutput = (adapter: string, message: string) =>
new LLMError({
module: ADAPTER,
module: adapter,
method: "generate",
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
reason: new InvalidProviderOutputReason({ message, route: adapter }),
})
const applyQuery = (url: string, query: Record<string, string> | undefined) => {
@@ -110,6 +145,7 @@ const PROTOCOL_BODY_FIELDS = new Set([
"moderation",
"output_format",
"output_compression",
"user_id",
])
const bodyWithOverlay = Effect.fn("OpenAIImages.bodyWithOverlay")(function* (
@@ -126,15 +162,22 @@ const bodyWithOverlay = Effect.fn("OpenAIImages.bodyWithOverlay")(function* (
})
export const model = (input: ModelInput) => {
const protocol = input.protocol ?? "openai"
const adapter = protocol === "openai" ? ADAPTER : "zai-images"
const name = protocol === "openai" ? "OpenAI" : "Z.ai"
const route: ImageRoute = {
id: ADAPTER,
id: adapter,
generate: Effect.fn("OpenAIImages.generate")(function* (request: ImageRequest, execute) {
if (request.aspectRatio !== undefined)
return yield* ProviderShared.invalidRequest("OpenAI Images does not support the common aspectRatio option")
return yield* ProviderShared.invalidRequest(`${name} Images does not support the common aspectRatio option`)
if (request.seed !== undefined)
return yield* ProviderShared.invalidRequest("OpenAI Images does not support the common seed option")
return yield* ProviderShared.invalidRequest(`${name} Images does not support the common seed option`)
if (protocol === "zai" && request.count !== undefined)
return yield* ProviderShared.invalidRequest("Z.ai Images does not support the common count option")
const requestBody = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIImageBody))(body(request))
const requestBody = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIImageBody))(
body(request, protocol),
)
const http = mergeHttpOptions(request.model.defaults?.http, request.http)
const overlaidBody = yield* bodyWithOverlay(requestBody, http?.body)
const text = ProviderShared.encodeJson(overlaidBody)
@@ -153,16 +196,19 @@ export const model = (input: ModelInput) => {
),
)
const payload = yield* response.json.pipe(
Effect.mapError(() => invalidOutput("Failed to read the OpenAI Images response")),
Effect.mapError(() => invalidOutput(adapter, `Failed to read the ${name} Images response`)),
)
const decoded = yield* Schema.decodeUnknownEffect(OpenAIImageResponse)(payload).pipe(
Effect.mapError(() => invalidOutput("OpenAI Images returned an invalid response")),
Effect.mapError(() => invalidOutput(adapter, `${name} Images returned an invalid response`)),
)
const format = decoded.output_format ?? providerOptions(request).outputFormat ?? "png"
const format =
protocol === "zai" ? "jpeg" : (decoded.output_format ?? providerOptions(request).outputFormat ?? "png")
const images = yield* Effect.forEach(decoded.data, (item, index) => {
if (item.b64_json)
return Effect.fromResult(Encoding.decodeBase64(item.b64_json)).pipe(
Effect.mapError(() => invalidOutput(`OpenAI Images result ${index} contains invalid base64 data`)),
Effect.mapError(() =>
invalidOutput(adapter, `${name} Images result ${index} contains invalid base64 data`),
),
Effect.map(
(data) =>
new GeneratedImage({
@@ -182,13 +228,13 @@ export const model = (input: ModelInput) => {
item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
}),
)
return Effect.fail(invalidOutput(`OpenAI Images result ${index} has neither image data nor a URL`))
return Effect.fail(invalidOutput(adapter, `${name} Images result ${index} has neither image data nor a URL`))
})
if (images.length === 0) return yield* invalidOutput("OpenAI Images returned no images")
if (images.length === 0) return yield* invalidOutput(adapter, `${name} Images returned no images`)
return new ImageResponse({
images,
usage:
decoded.usage === undefined
protocol === "zai" || decoded.usage === undefined
? undefined
: new Usage({
inputTokens: decoded.usage.input_tokens,
@@ -196,11 +242,21 @@ export const model = (input: ModelInput) => {
totalTokens: decoded.usage.total_tokens,
providerMetadata: { openai: decoded.usage },
}),
providerMetadata: { openai: { outputFormat: format } },
providerMetadata:
protocol === "openai"
? { openai: { outputFormat: format } }
: {
zai: {
created: decoded.created,
id: decoded.id,
requestID: decoded.request_id,
contentFilter: decoded.content_filter,
},
},
})
}),
}
return ImageModel.make({ id: input.id, provider: "openai", route, defaults: input.defaults })
return ImageModel.make({ id: input.id, provider: protocol, route, defaults: input.defaults })
}
export const OpenAIImages = {
+1
View File
@@ -15,3 +15,4 @@ export * as OpenAICompatible from "./openai-compatible"
export * as OpenAICompatibleResponses from "./openai-compatible-responses"
export * as OpenRouter from "./openrouter"
export * as XAI from "./xai"
export * as ZAI from "./zai"
+45
View File
@@ -0,0 +1,45 @@
import { OpenAIImages, type ZAIImageOptions } from "../protocols/openai-images"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { HttpOptions, ProviderID, type ModelID } from "../schema"
export const id = ProviderID.make("zai")
export interface ImageConfig {
readonly providerOptions?: ZAIImageOptions
}
export type Config = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
readonly image?: ImageConfig
}
export type { ZAIImageOptions } from "../protocols/openai-images"
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "ZAI_API_KEY")
export const configure = (input: Config = {}) => {
const image = (modelID: string | ModelID) =>
OpenAIImages.model({
id: modelID,
protocol: "zai",
auth: auth(input),
baseURL: input.baseURL ?? "https://api.z.ai/api/paas/v4",
headers: input.headers,
defaults: {
providerOptions:
input.image?.providerOptions === undefined ? undefined : { zai: { ...input.image.providerOptions } },
http: input.http === undefined ? undefined : HttpOptions.make(input.http),
},
})
return {
id,
image,
configure,
}
}
export const provider = configure()
export const image = provider.image
@@ -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\"}"
}
}
]
}
@@ -0,0 +1,35 @@
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: { providerOptions: { quality: "standard", userID: "opencode-image-test" } },
}).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.",
size: { width: 1024, height: 1024 },
})
expect(response.images).toHaveLength(1)
expect(response.image?.mediaType).toBe("image/jpeg")
expect(response.image?.data).toBeString()
expect(response.image?.data).toStartWith("https://")
expect(response.providerMetadata?.zai).toBeDefined()
}),
)
})
@@ -0,0 +1,73 @@
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 } 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: { request_metadata: "value" }, query: { trace: "default" } },
image: { providerOptions: { quality: "standard", userID: "user-123" } },
}).image("glm-image"),
prompt: "A red circle on a white background",
size: { width: 1280, height: 1280 },
providerOptions: { zai: { quality: "hd" } },
http: { headers: { "x-request": "yes" }, query: { trace: "request" } },
})
expect(response.images).toHaveLength(1)
expect(response.image?.mediaType).toBe("image/jpeg")
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: "assistant", level: 3 }],
},
})
}).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",
size: "1280x1280",
quality: "hd",
user_id: "user-123",
request_metadata: "value",
})
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: "assistant", level: 3 }],
}),
{ headers: { "content-type": "application/json" } },
)
}),
),
),
),
),
),
)
})