MAI Image 2.5 (Private Preview) — Edit + Generate Notebook
Model card date: May 2026
Release date: June 2, 2026
MAI-Image-2.5 is a 20B-parameter diffusion model for both:
- high-quality text-to-image generation, and
- precise, controllable image-to-image editing.
Model-card highlights reflected in this notebook:
- Architecture: diffusion + flow-matching objective
- Inputs: text, plus image input for editing
- Context length: 32K tokens
- Output limit: total pixels must be <= 1,048,576 (equivalent to 1024x1024). Either dimension may exceed 1024 if total stays within budget.
- Strong focus areas: surgical edits, layout preservation, text updates, artifact cleanup, portrait/product quality
- Quality signal: top-3 ranking on LMArena text-to-image and image-editing leaderboards (per model card)
Private Preview Terms (must follow)
- Internal-only use; no external end-user access.
- Private preview models are not for production deployments.
- Follow Microsoft license terms and Generative AI code of conduct.
- Abuse monitoring and content filtering remain enabled.
- Preview is as-is and may change or be discontinued without notice.
- Do not send regulated personal data in preview feedback.
1. Setup
Environment variables
| Variable | Required | Secret | Purpose |
|---|---|---|---|
MAI_IMAGE_25_ENDPOINT |
Yes | No | MAI Image 2.5 endpoint (Cognitive endpoint format recommended). |
MAI_IMAGE_25_API_KEY |
Yes | Yes | API key for MAI Image 2.5 preview calls. |
MAI_IMAGE_25_DEPLOYMENT_NAME |
Yes | No | Deployment/model name used in request payloads. |
IMAGE_OUTPUT_DIR |
Optional | No | Output directory for generated files; defaults to media/mai-image-2-5. |
MAI_IMAGE_25_INPUT_IMAGE |
Optional | No | Input image for edit flow; defaults to media/mai-image-2-5/01-generated-example.png. |
MAI_IMAGE_25_EDIT_SIZE |
Optional | No | Edit image size (for example 1024x1024). |
Do not commit .env or deployment.env files with secrets.
# %pip install -q requests python-dotenv pillow
import os
import base64
import mimetypes
import socket
from urllib.parse import urlparse
from pathlib import Path
import requests
from dotenv import load_dotenv
from IPython.display import Image as IPImage, display
ENV_PATH = 'deployment.env' if os.path.exists('deployment.env') else os.path.join('..', 'deployment.env')
load_dotenv(ENV_PATH, override=True)
MAI_IMAGE_25_ENDPOINT = os.getenv('MAI_IMAGE_25_ENDPOINT')
MAI_IMAGE_25_DEPLOYMENT_NAME = os.getenv('MAI_IMAGE_25_DEPLOYMENT_NAME')
MAI_IMAGE_25_API_KEY = os.getenv('MAI_IMAGE_25_API_KEY')
image_output_env = os.getenv('IMAGE_OUTPUT_DIR')
IMAGE_OUTPUT_DIR = Path(image_output_env) if image_output_env else Path('media') / 'mai-image-2-5'
IMAGE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
input_image_env = os.getenv('MAI_IMAGE_25_INPUT_IMAGE')
INPUT_IMAGE_PATH = Path(input_image_env) if input_image_env else IMAGE_OUTPUT_DIR / '01-generated-example.png'
DEFAULT_EDIT_SIZE = os.getenv('MAI_IMAGE_25_EDIT_SIZE', '1024x1024')
assert MAI_IMAGE_25_ENDPOINT, 'Set MAI_IMAGE_25_ENDPOINT in deployment.env'
assert MAI_IMAGE_25_API_KEY, 'Set MAI_IMAGE_25_API_KEY in deployment.env (MAI Image 2.5 currently uses API key auth in preview)'
def auth_headers(content_type: str | None = None) -> dict:
h = {'api-key': MAI_IMAGE_25_API_KEY}
if content_type:
h['Content-Type'] = content_type
return h
def preflight_endpoint_dns(endpoint: str) -> None:
host = urlparse(endpoint).hostname
try:
socket.getaddrinfo(host, 443)
except Exception as ex:
raise RuntimeError(
f'Endpoint DNS resolution failed for {host}: {ex}. '
'Use the Cognitive endpoint format (e.g. https://westcentralus.api.cognitive.microsoft.com/).'
)
preflight_endpoint_dns(MAI_IMAGE_25_ENDPOINT)
print('Endpoint:', MAI_IMAGE_25_ENDPOINT)
print('Deployment:', MAI_IMAGE_25_DEPLOYMENT_NAME)
print('Auth mode: API key')
print('Output dir:', IMAGE_OUTPUT_DIR)
print('Default edit size:', DEFAULT_EDIT_SIZE)
2. API behavior note (important)
- Image edit endpoint (
/mai/v1/images/edits) usessize(for example,1024x1024). - Text-to-image generation endpoint (
/mai/v1/images/generations) useswidthandheight. - For both paths, keep total pixel budget <= 1024x1024.
Illustrative examples from one run:
The images below are illustrative examples from one run. Your output may differ because image generation and editing are non-deterministic.


3. Text-to-Image Helper (width + height)
def _validate_pixel_budget(width: int, height: int, source: str) -> None:
assert width > 0 and height > 0, f'{source}: width and height must be positive'
assert width * height <= 1_048_576, f'{source}: width*height must be <= 1,048,576'
def generate_image_25(prompt: str, out_file: str, width: int = 1024, height: int = 1024) -> Path:
_validate_pixel_budget(width, height, 'generation')
url = f"{MAI_IMAGE_25_ENDPOINT.rstrip('/')}/mai/v1/images/generations"
payload = {
'model': MAI_IMAGE_25_DEPLOYMENT_NAME,
'prompt': prompt,
'width': width,
'height': height,
}
resp = requests.post(url, headers=auth_headers('application/json'), json=payload, timeout=180)
if not resp.ok:
raise requests.HTTPError(f'Generation failed with {resp.status_code}: {resp.text}', response=resp)
data = resp.json().get('data', [])
assert data and data[0].get('b64_json'), f'No image output in response: {resp.text[:500]}'
out_path = IMAGE_OUTPUT_DIR / out_file
out_path.write_bytes(base64.b64decode(data[0]['b64_json']))
return out_path
4. Image Edit Helper (size)
def _parse_size(size: str) -> tuple[int, int]:
parts = size.lower().split('x')
assert len(parts) == 2, "size must be in '<width>x<height>' format, e.g. 1024x1024"
w, h = int(parts[0]), int(parts[1])
return w, h
def edit_image_25(prompt: str, input_image: Path, out_file: str, size: str = DEFAULT_EDIT_SIZE) -> Path:
w, h = _parse_size(size)
_validate_pixel_budget(w, h, 'edit')
url = f"{MAI_IMAGE_25_ENDPOINT.rstrip('/')}/mai/v1/images/edits"
mime = mimetypes.guess_type(str(input_image))[0] or 'image/png'
assert input_image.exists(), f'Input image not found: {input_image}'
headers = auth_headers()
with input_image.open('rb') as f:
files = {'image': (input_image.name, f, mime)}
form = {
'prompt': prompt,
'model': MAI_IMAGE_25_DEPLOYMENT_NAME,
'size': size,
}
resp = requests.post(url, headers=headers, files=files, data=form, timeout=300)
if not resp.ok:
raise requests.HTTPError(f'Edit failed with {resp.status_code}: {resp.text}', response=resp)
data = resp.json().get('data', [])
assert data and data[0].get('b64_json'), 'b64_json missing or null (common with bad decoding flow)'
out_path = IMAGE_OUTPUT_DIR / out_file
out_path.write_bytes(base64.b64decode(data[0]['b64_json']))
return out_path
5. Run a generation sample
generated = generate_image_25(
prompt='A photorealistic concept art poster of an Azure AI lab at sunset, cinematic lighting',
out_file='mai_image25_generation.png',
width=1024,
height=1024,
)
print('Generated image:', generated)
display(IPImage(filename=str(generated), width=512))
INPUT_IMAGE_PATH=generatedExample response shape:
{
"created": 0,
"data": [
{
"b64_json": "<redacted-base64-image>"
}
]
}
6. Run an edit sample (requires input image)
if INPUT_IMAGE_PATH.exists():
edited = edit_image_25(
prompt='Turn this into a clean futuristic product shot with studio lighting',
input_image=INPUT_IMAGE_PATH,
out_file='mai_image25_edited.png',
size=DEFAULT_EDIT_SIZE,
)
print('Edited image:', edited)
display(IPImage(filename=str(edited), width=512))
else:
print(f'Skipping edit run: input image not found at {INPUT_IMAGE_PATH}')
Example response shape:
{
"created": 0,
"data": [
{
"b64_json": "<redacted-base64-image>"
}
]
}
7. Troubleshooting
| Error | Resolution |
|---|---|
DeploymentModelNotSupported |
Subscription not whitelisted or model name/version mismatch. Verify deployment and model availability. |
| Output image is 0-3 bytes | b64_json is null or decode flow is wrong. Save response JSON first, then decode. |
base64: unrecognized option --ignore-garbage |
On macOS use base64 -d instead of GNU flags. |
401/403 |
Invalid/expired API key or wrong endpoint. Re-check MAI_IMAGE_25_API_KEY and MAI_IMAGE_25_ENDPOINT. |
8. Usage, safety, and out-of-scope reminders
Primary use cases aligned to model card:
- Creative text-to-image generation
- Surgical image editing (object removal/replacement, inpainting, text updates, attribute changes)
- Product/branding visuals, portraits, and production design workflows
Out-of-scope / not allowed:
- Deceptive impersonation or misleading identity content
- Illegal or policy-violating content generation
- Harmful or abusive content generation
Responsible AI notes:
- This model includes layered safety guardrails (prompt and output filtering) in deployed systems.
- Continue to review outputs and prompts for policy compliance before downstream use.
Tags
mai models inference multimodal