Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c56e798b79 | |||
| efda817636 |
+14
-1
@@ -45,6 +45,19 @@ const program = Effect.gen(function* () {
|
||||
})
|
||||
```
|
||||
|
||||
Z.ai uses the same provider-neutral response through its image facade:
|
||||
|
||||
```ts
|
||||
import { Image } from "@opencode-ai/ai"
|
||||
import { ZAI } from "@opencode-ai/ai/providers"
|
||||
|
||||
const model = ZAI.configure({ apiKey: process.env.ZAI_API_KEY }).image("glm-image")
|
||||
const response = yield * Image.generate({ model, prompt: "A paper-cut forest at dawn" })
|
||||
```
|
||||
|
||||
Z.ai returns temporary output URLs that expire after 30 days. Download and persist generated images 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
|
||||
@@ -145,7 +158,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
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ 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,
|
||||
@@ -38,6 +45,15 @@ const OpenAIImageBody = Schema.Struct({
|
||||
})
|
||||
export type OpenAIImageBody = Schema.Schema.Type<typeof OpenAIImageBody>
|
||||
|
||||
const ZAIImageBody = Schema.Struct({
|
||||
model: Schema.String,
|
||||
prompt: Schema.String,
|
||||
size: Schema.optional(Schema.String),
|
||||
quality: Schema.optional(Schema.Literals(["hd", "standard"])),
|
||||
user_id: Schema.optional(Schema.String.check(Schema.isMinLength(6), Schema.isMaxLength(128))),
|
||||
})
|
||||
export type ZAIImageBody = Schema.Schema.Type<typeof ZAIImageBody>
|
||||
|
||||
const OpenAIImageResponse = Schema.Struct({
|
||||
data: Schema.Array(
|
||||
Schema.Struct({
|
||||
@@ -58,8 +74,24 @@ const OpenAIImageResponse = Schema.Struct({
|
||||
),
|
||||
})
|
||||
|
||||
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.Literals(["assistant", "user", "history"])),
|
||||
level: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 3 }))),
|
||||
}),
|
||||
),
|
||||
),
|
||||
})
|
||||
|
||||
export interface ModelInput {
|
||||
readonly id: string
|
||||
readonly protocol?: ImageProtocol
|
||||
readonly auth: AuthDefinition
|
||||
readonly baseURL?: string
|
||||
readonly headers?: Record<string, string>
|
||||
@@ -71,7 +103,12 @@ 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 openaiBody = (request: ImageRequest): OpenAIImageBody => {
|
||||
const options = providerOptions(request)
|
||||
return {
|
||||
model: request.model.id,
|
||||
@@ -86,11 +123,22 @@ const body = (request: ImageRequest): OpenAIImageBody => {
|
||||
}
|
||||
}
|
||||
|
||||
const invalidOutput = (message: string) =>
|
||||
const zaiBody = (request: ImageRequest): ZAIImageBody => {
|
||||
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 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) => {
|
||||
@@ -100,7 +148,7 @@ const applyQuery = (url: string, query: Record<string, string> | undefined) => {
|
||||
return next.toString()
|
||||
}
|
||||
|
||||
const PROTOCOL_BODY_FIELDS = new Set([
|
||||
const OPENAI_BODY_FIELDS = new Set([
|
||||
"model",
|
||||
"prompt",
|
||||
"n",
|
||||
@@ -111,13 +159,15 @@ const PROTOCOL_BODY_FIELDS = new Set([
|
||||
"output_format",
|
||||
"output_compression",
|
||||
])
|
||||
const ZAI_BODY_FIELDS = new Set(["model", "prompt", "size", "quality", "user_id"])
|
||||
|
||||
const bodyWithOverlay = Effect.fn("OpenAIImages.bodyWithOverlay")(function* (
|
||||
imageBody: OpenAIImageBody,
|
||||
imageBody: Record<string, unknown>,
|
||||
overlay: Record<string, unknown> | undefined,
|
||||
ownedFields: ReadonlySet<string>,
|
||||
) {
|
||||
if (!overlay) return imageBody
|
||||
const reserved = Object.keys(overlay).filter((key) => PROTOCOL_BODY_FIELDS.has(key))
|
||||
const reserved = Object.keys(overlay).filter((key) => ownedFields.has(key))
|
||||
if (reserved.length > 0)
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
`http.body cannot overlay protocol-owned field(s): ${reserved.join(", ")}`,
|
||||
@@ -126,17 +176,29 @@ 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 =
|
||||
protocol === "openai"
|
||||
? yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenAIImageBody))(openaiBody(request))
|
||||
: yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(ZAIImageBody))(zaiBody(request))
|
||||
const http = mergeHttpOptions(request.model.defaults?.http, request.http)
|
||||
const overlaidBody = yield* bodyWithOverlay(requestBody, http?.body)
|
||||
const overlaidBody = yield* bodyWithOverlay(
|
||||
requestBody,
|
||||
http?.body,
|
||||
protocol === "openai" ? OPENAI_BODY_FIELDS : ZAI_BODY_FIELDS,
|
||||
)
|
||||
const text = ProviderShared.encodeJson(overlaidBody)
|
||||
const url = applyQuery(`${(input.baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "")}${PATH}`, http?.query)
|
||||
const headers = yield* Auth.toEffect(input.auth)({
|
||||
@@ -153,23 +215,34 @@ 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")),
|
||||
)
|
||||
const format = decoded.output_format ?? providerOptions(request).outputFormat ?? "png"
|
||||
const decoded = yield* (
|
||||
protocol === "openai"
|
||||
? Schema.decodeUnknownEffect(OpenAIImageResponse)(payload)
|
||||
: Schema.decodeUnknownEffect(ZAIImageResponse)(payload)
|
||||
).pipe(Effect.mapError(() => invalidOutput(adapter, `${name} Images returned an invalid response`)))
|
||||
const format =
|
||||
protocol === "zai"
|
||||
? "jpeg"
|
||||
: (("output_format" in decoded ? decoded.output_format : undefined) ??
|
||||
providerOptions(request).outputFormat ??
|
||||
"png")
|
||||
const images = yield* Effect.forEach(decoded.data, (item, index) => {
|
||||
if (item.b64_json)
|
||||
if ("b64_json" in item && 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({
|
||||
mediaType: `image/${format}`,
|
||||
data,
|
||||
providerMetadata:
|
||||
item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
||||
"revised_prompt" in item && item.revised_prompt !== undefined
|
||||
? { openai: { revisedPrompt: item.revised_prompt } }
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -179,16 +252,18 @@ export const model = (input: ModelInput) => {
|
||||
mediaType: `image/${format}`,
|
||||
data: item.url,
|
||||
providerMetadata:
|
||||
item.revised_prompt === undefined ? undefined : { openai: { revisedPrompt: item.revised_prompt } },
|
||||
"revised_prompt" in item && item.revised_prompt !== undefined
|
||||
? { openai: { revisedPrompt: item.revised_prompt } }
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
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" || !("usage" in decoded) || decoded.usage === undefined
|
||||
? undefined
|
||||
: new Usage({
|
||||
inputTokens: decoded.usage.input_tokens,
|
||||
@@ -196,11 +271,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: "created" in decoded ? decoded.created : undefined,
|
||||
id: "id" in decoded ? decoded.id : undefined,
|
||||
requestID: "request_id" in decoded ? decoded.request_id : undefined,
|
||||
contentFilter: "content_filter" in decoded ? decoded.content_filter : undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
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 = {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,147 @@
|
||||
import { describe, expect } from "bun:test"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClientRequest } from "effect/unstable/http"
|
||||
import { Image, ImageClient } from "../../src"
|
||||
import { OpenAI, 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: { 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" } },
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("validates Z.ai-owned request fields without reserving them for OpenAI", () =>
|
||||
Effect.gen(function* () {
|
||||
const invalid = yield* Image.generate({
|
||||
model: ZAI.configure({ apiKey: "test", image: { providerOptions: { userID: "short" } } }).image("model"),
|
||||
prompt: "test",
|
||||
}).pipe(Effect.provide(ImageClient.layer.pipe(Layer.provide(fixedResponse("{}")))), Effect.flip)
|
||||
expect(invalid.reason._tag).toBe("InvalidRequest")
|
||||
|
||||
const openaiQuality = yield* Image.generate({
|
||||
model: OpenAI.configure({ apiKey: "test" }).image("model"),
|
||||
prompt: "test",
|
||||
providerOptions: { openai: { quality: "standard" } },
|
||||
}).pipe(Effect.provide(ImageClient.layer.pipe(Layer.provide(fixedResponse("{}")))), Effect.flip)
|
||||
expect(openaiQuality.reason._tag).toBe("InvalidRequest")
|
||||
|
||||
const zaiOverlay = yield* Image.generate({
|
||||
model: ZAI.configure({ apiKey: "test" }).image("model"),
|
||||
prompt: "test",
|
||||
http: { body: { user_id: "overlay-user" } },
|
||||
}).pipe(Effect.provide(ImageClient.layer.pipe(Layer.provide(fixedResponse("{}")))), Effect.flip)
|
||||
expect(zaiOverlay.reason).toMatchObject({
|
||||
_tag: "InvalidRequest",
|
||||
message: "http.body cannot overlay protocol-owned field(s): user_id",
|
||||
})
|
||||
|
||||
const request = yield* Image.generate({
|
||||
model: OpenAI.configure({ apiKey: "test" }).image("model"),
|
||||
prompt: "test",
|
||||
http: { body: { user_id: "overlay-user" } },
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
ImageClient.layer.pipe(
|
||||
Layer.provide(
|
||||
dynamicResponse((input) => {
|
||||
expect(JSON.parse(input.text)).toMatchObject({ user_id: "overlay-user" })
|
||||
return Effect.succeed(
|
||||
input.respond(JSON.stringify({ data: [{ url: "https://example.test/image.jpg" }] }), {
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
expect(request.image?.data).toBe("https://example.test/image.jpg")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects invalid Z.ai content filter structures", () =>
|
||||
Effect.gen(function* () {
|
||||
const model = ZAI.configure({ apiKey: "test" }).image("model")
|
||||
const payloads = [
|
||||
{ data: [{ url: "https://example.test/image.jpg" }], content_filter: [{ role: "system", level: 1 }] },
|
||||
{ data: [{ url: "https://example.test/image.jpg" }], content_filter: [{ role: "user", level: 1.5 }] },
|
||||
{ data: [{ url: "https://example.test/image.jpg" }], content_filter: [{ role: "history", level: 4 }] },
|
||||
]
|
||||
|
||||
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"))),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user