feat(ai): add Z.ai image generation (#37780)

Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-07-20 15:53:21 -05:00
committed by GitHub
co-authored by Aiden Cline
parent 4c9a4309ce
commit fa4ec98e1d
8 changed files with 391 additions and 2 deletions
+21 -1
View File
@@ -110,6 +110,26 @@ future string values and arbitrary native Gemini `generationConfig` fields remai
their mapped aliases, and `http.body` is the final deep overlay. The selected model ID is sent to Gemini
`generateContent` without a local allowlist.
Z.ai image models infer open Z.ai-native options from the selected model:
```ts
yield *
Image.generate({
model: ZAI.configure({ apiKey }).image("any-model-id"),
prompt,
options: {
quality: "hd",
userID: "user-123",
future_option: true,
},
http,
})
```
Z.ai does not include trustworthy MIME metadata for output URLs, so generated images use
`application/octet-stream`. Output URLs expire after 30 days; download and persist them promptly if they must
remain available.
Conversational image generation remains part of the LLM interaction. OpenAI Responses exposes it through its hosted image tool:
```ts
@@ -210,7 +230,7 @@ const gateway = CloudflareAIGateway.configure({
}).model("workers-ai/@cf/meta/llama-3.1-8b-instruct")
```
Included providers: OpenAI, Anthropic, Google (Gemini), Google Vertex Gemini and Anthropic, Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, plus generic OpenAI-compatible Chat and Responses entrypoints and an Anthropic Messages-compatible entrypoint.
Included providers: OpenAI, Anthropic, Google (Gemini), Google Vertex Gemini and Anthropic, Amazon Bedrock, Azure OpenAI, Cloudflare AI Gateway, Cloudflare Workers AI, GitHub Copilot, OpenRouter, xAI, Z.ai, plus generic OpenAI-compatible Chat and Responses entrypoints and an Anthropic Messages-compatible entrypoint.
### Package-like entrypoints
+129
View File
@@ -0,0 +1,129 @@
import { Effect, Schema } from "effect"
import { Headers, HttpClientRequest } from "effect/unstable/http"
import { GeneratedImage, ImageModel, ImageResponse, type ImageRequestFor, type ImageRoute } from "../image"
import { Auth, type Definition as AuthDefinition } from "../route/auth"
import { InvalidProviderOutputReason, LLMError, mergeHttpOptions, mergeJsonRecords, type HttpOptions } from "../schema"
import { ProviderShared } from "./shared"
const ADAPTER = "zai-images"
export const DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4"
export const PATH = "/images/generations"
export type ZAIImageString<Known extends string> = Known | (string & {})
export type ZAIImageOptions = {
readonly size?: ZAIImageString<
"1024x1024" | "768x1344" | "864x1152" | "1344x768" | "1152x864" | "1440x720" | "720x1440"
>
readonly quality?: ZAIImageString<"hd" | "standard">
readonly userID?: string
} & Record<string, unknown>
type ZAIImageBody = Record<string, unknown> & {
readonly model: string
readonly prompt: string
}
const ZAIImageResponse = Schema.Struct({
created: Schema.optional(Schema.Int),
id: Schema.optional(Schema.String),
request_id: Schema.optional(Schema.String),
data: Schema.Array(Schema.Struct({ url: 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 auth: AuthDefinition
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions
}
const nativeOptions = (options: ZAIImageOptions | undefined) => {
if (!options) return undefined
const { userID, ...native } = options
return {
user_id: userID,
...native,
}
}
const invalidOutput = (message: string) =>
new LLMError({
module: ADAPTER,
method: "generate",
reason: new InvalidProviderOutputReason({ message, route: ADAPTER }),
})
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()
}
export const model = (input: ModelInput) => {
const route: ImageRoute<ZAIImageOptions> = {
id: ADAPTER,
generate: Effect.fn("ZAIImages.generate")(function* (request: ImageRequestFor<ZAIImageOptions>, execute) {
const http = mergeHttpOptions(request.model.http, request.http)
const requestBody = mergeJsonRecords(
{ model: request.model.id, prompt: request.prompt },
nativeOptions(request.options),
http?.body,
) as ZAIImageBody
const text = ProviderShared.encodeJson(requestBody)
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 Z.ai Images response")),
)
const decoded = yield* Schema.decodeUnknownEffect(ZAIImageResponse)(payload).pipe(
Effect.mapError(() => invalidOutput("Z.ai Images returned an invalid response")),
)
if (decoded.data.length === 0) return yield* invalidOutput("Z.ai Images returned no images")
return new ImageResponse({
images: decoded.data.map(
(item) =>
new GeneratedImage({
mediaType: "application/octet-stream",
data: item.url,
}),
),
providerMetadata: {
zai: {
created: decoded.created,
id: decoded.id,
requestID: decoded.request_id,
contentFilter: decoded.content_filter,
},
},
})
}),
}
return ImageModel.make<ZAIImageOptions>({ id: input.id, provider: "zai", route, http: input.http })
}
export const ZAIImages = {
model,
} as const
+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"
+35
View File
@@ -0,0 +1,35 @@
import { ZAIImages } from "../protocols/zai-images"
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import { HttpOptions, ProviderID, type ModelID } from "../schema"
export const id = ProviderID.make("zai")
export type Config = ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly headers?: Record<string, string>
readonly http?: HttpOptions.Input
}
export type { ZAIImageOptions } from "../protocols/zai-images"
const auth = (options: ProviderAuthOption<"optional">) => AuthOptions.bearer(options, "ZAI_API_KEY")
export const configure = (input: Config = {}) => {
const image = (modelID: string | ModelID) =>
ZAIImages.model({
id: modelID,
auth: auth(input),
baseURL: input.baseURL,
headers: input.headers,
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\"}"
}
}
]
}
+15 -1
View File
@@ -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 } })
@@ -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()
}),
)
})
@@ -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"))),
),
)
}),
)
})