XuanShu API
Image generation and editing examples
Use gpt-image-2 for text-to-image, image-to-image, and image editing, with runnable streaming Python and Node.js examples plus troubleshooting.
1. Get an API key
- If you do not have an account yet, register at /register.
- Create a key at /keys and replace <YOUR_API_KEY> in the examples with the full value.
- Check /available-channels to confirm gpt-image-2 is visible for the current key.
2. Text-to-image
Give only a prompt, with no reference image. The response streams over SSE; partial_image events report progress, and the final event carries the image as b64_json.
import base64
import json
import urllib.request
from pathlib import Path
API_URL = "https://www.xuanshuapi.com/v1/images/generations"
API_KEY = "<YOUR_API_KEY>"
def iter_sse(response):
buffer = ""
while chunk := response.read(4096):
buffer += chunk.decode("utf-8", errors="replace")
frames = buffer.split("\n\n")
buffer = frames.pop()
for frame in frames:
payload = [
line[5:].strip()
for line in frame.splitlines()
if line.startswith("data:")
]
data = "\n".join(payload).strip()
if data and data != "[DONE]":
yield data
body = {
"model": "gpt-image-2",
"prompt": "一只在太空里漂浮的猫,科技感插画风格",
"n": 1,
"size": "1024x1024",
"stream": True,
"response_format": "b64_json",
}
request = urllib.request.Request(
API_URL,
data=json.dumps(body).encode("utf-8"),
method="POST",
headers={
"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
)
with urllib.request.urlopen(request, timeout=900) as response:
for data in iter_sse(response):
event = json.loads(data)
if event.get("type") == "image_generation.partial_image":
print(".", end="", flush=True)
image = (
event.get("b64_json")
or ((event.get("data") or [{}])[0]).get("b64_json")
or (event.get("item") or {}).get("result")
)
if image:
Path("generated-image.png").write_bytes(base64.b64decode(image))
print("\n已保存:generated-image.png")
breakimport { writeFile } from "node:fs/promises";
const API_URL = "https://www.xuanshuapi.com/v1/images/generations";
const API_KEY = "<YOUR_API_KEY>";
async function* readSse(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() || "";
for (const frame of frames) {
const data = frame
.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trim())
.join("\n");
if (data && data !== "[DONE]") yield data;
}
}
}
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: "Bearer " + API_KEY,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({
model: "gpt-image-2",
prompt: "一只在太空里漂浮的猫,科技感插画风格",
n: 1,
size: "1024x1024",
stream: true,
response_format: "b64_json",
}),
});
if (!response.ok) throw new Error(await response.text());
for await (const data of readSse(response)) {
const event = JSON.parse(data);
if (event.type === "image_generation.partial_image") process.stdout.write(".");
const image = event.b64_json ?? event.data?.[0]?.b64_json ?? event.item?.result;
if (image) {
await writeFile("generated-image.png", Buffer.from(image, "base64"));
console.log("\n已保存:generated-image.png");
break;
}
}3. Image-to-image (JSON reference image)
Pass the reference image via images[].image_url; multiple images are supported, and data URLs also work. images[].file_id is not supported and returns an error.
import base64
import json
import urllib.request
from pathlib import Path
API_URL = "https://www.xuanshuapi.com/v1/images/edits"
API_KEY = "<YOUR_API_KEY>"
def iter_sse(response):
buffer = ""
while chunk := response.read(4096):
buffer += chunk.decode("utf-8", errors="replace")
frames = buffer.split("\n\n")
buffer = frames.pop()
for frame in frames:
payload = [
line[5:].strip()
for line in frame.splitlines()
if line.startswith("data:")
]
data = "\n".join(payload).strip()
if data and data != "[DONE]":
yield data
# 参考图用 images[].image_url 传入,支持多张,也可传 data URL。
# 注意:images[].file_id 不受支持。
body = {
"model": "gpt-image-2",
"prompt": "参考这张图,生成一张更精致的科技风品牌图。",
"images": [{"image_url": "https://www.xuanshuapi.com/brand/og-cover.png"}],
"n": 1,
"size": "1024x1024",
"quality": "auto",
"stream": True,
"response_format": "b64_json",
}
request = urllib.request.Request(
API_URL,
data=json.dumps(body).encode("utf-8"),
method="POST",
headers={
"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json",
"Accept": "text/event-stream",
},
)
with urllib.request.urlopen(request, timeout=900) as response:
for data in iter_sse(response):
event = json.loads(data)
if event.get("type") == "image_generation.partial_image":
print(".", end="", flush=True)
image = (
event.get("b64_json")
or ((event.get("data") or [{}])[0]).get("b64_json")
or (event.get("item") or {}).get("result")
)
if image:
Path("generated-image.png").write_bytes(base64.b64decode(image))
print("\n已保存:generated-image.png")
break4. Image editing (multipart upload)
Upload an image file directly and edit it by prompt. This path uses multipart/form-data; do not set Content-Type by hand, let FormData generate the boundary.
import { writeFile } from "node:fs/promises";
const API_URL = "https://www.xuanshuapi.com/v1/images/edits";
const API_KEY = "<YOUR_API_KEY>";
const SOURCE_URL = "https://www.xuanshuapi.com/brand/og-cover.png";
async function* readSse(response) {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const frames = buffer.split(/\r?\n\r?\n/);
buffer = frames.pop() || "";
for (const frame of frames) {
const data = frame
.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trim())
.join("\n");
if (data && data !== "[DONE]") yield data;
}
}
}
const source = await fetch(SOURCE_URL);
const sourceType = source.headers.get("content-type") || "image/png";
const sourceFile = new File(
[await source.arrayBuffer()], "source.png", { type: sourceType });
const form = new FormData();
form.append("image", sourceFile);
form.append("prompt", "把图片整体色调改为蓝色。");
form.append("model", "gpt-image-2");
form.append("n", "1");
form.append("quality", "auto");
form.append("size", "1024x1024");
form.append("stream", "true");
form.append("response_format", "b64_json");
const response = await fetch(API_URL, {
method: "POST",
headers: { Authorization: "Bearer " + API_KEY, Accept: "text/event-stream" },
body: form,
});
if (!response.ok) throw new Error(await response.text());
for await (const data of readSse(response)) {
const event = JSON.parse(data);
if (event.type === "image_generation.partial_image") process.stdout.write(".");
const image = event.b64_json ?? event.data?.[0]?.b64_json ?? event.item?.result;
if (image) {
await writeFile("edited-image.png", Buffer.from(image, "base64"));
console.log("\n已保存:edited-image.png");
break;
}
}5. Success criteria
The command exits cleanly and writes generated-image.png or edited-image.png in the current directory, and the image opens correctly. You should also see a successful gpt-image-2 call in /usage, with no 404, 401/403, or 429.
6. Troubleshooting
| Symptom | Check | Fix |
|---|---|---|
| 404 | The text-to-image endpoint is /v1/images/generations; image-to-image and editing use /v1/images/edits. Both include /v1. | Correct the URL and retry. |
| 400 missing reference image | When calling edits with JSON, images[].image_url is required. | Add the images array, or switch to a multipart upload of the image file. |
| file_id error | Neither images[].file_id nor mask.file_id is supported. | Use image_url or a data URL instead. |
| Model unavailable | Check /available-channels to confirm gpt-image-2 is visible for the current key. | Switch to an image model visible in the console. |
| 429 | Check balance, key limits, and concurrency at /usage. | Lower concurrency and wait for the rate limit window to reset. |
| No image in the response | In streaming responses, the final image may appear in b64_json, data[0].b64_json, or item.result. | Check all three locations like the examples do; write the file and break as soon as one is found. |