Files
fastgpt-python-sdk/fastgpt_client/images.py
Xin Wang 07f30af105 feat: support base64 and upload image input modes in chat
Add a unified way to send images in chat: inline base64 data URLs
(passed through natively) or auto-upload via image_input_mode="upload",
which replaces inline data URLs with hosted URLs using upload_chat_image.

- New fastgpt_client/images.py with content-part / data-URL helpers
- image_input_mode + appId/outLinkAuthData params on create_chat_completion
  (sync and async); upload failures fall back to inline base64
- Tests covering helpers, both modes, validation, and fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:50:37 +08:00

81 lines
2.9 KiB
Python

"""Helpers for attaching images to FastGPT chat messages.
FastGPT is OpenAI-compatible and accepts two ways to send an image inside a
message's ``content`` list as an ``image_url`` part:
1. **Inline base64** — a ``data:<mime>;base64,<payload>`` data URL. Nothing is
uploaded; the image travels inside the request body. Cheapest to send and
requires no ``appId``/``chatId``.
2. **Uploaded URL** — upload the bytes first (see
:meth:`ChatClient.upload_chat_image`) and reference the returned URL. Keeps
the request body small and lets the image be reused/previewed later.
These helpers build the ``image_url`` content parts for either method and let
you convert an inline data URL back into raw bytes (used when uploading).
"""
from __future__ import annotations
import base64
import binascii
import mimetypes
from pathlib import Path
from typing import Any
__all__ = [
"encode_image_data_url",
"is_data_url",
"decode_data_url",
"image_url_part",
"image_part_from_bytes",
"image_part_from_path",
]
_DATA_URL_PREFIX = "data:"
def encode_image_data_url(data: bytes, mime_type: str = "image/jpeg") -> str:
"""Return a ``data:<mime>;base64,<payload>`` URL for raw image bytes."""
payload = base64.b64encode(data).decode("ascii")
return f"{_DATA_URL_PREFIX}{mime_type};base64,{payload}"
def is_data_url(url: Any) -> bool:
"""True if ``url`` is an inline base64 data URL (``data:...``)."""
return isinstance(url, str) and url.startswith(_DATA_URL_PREFIX)
def decode_data_url(data_url: str) -> tuple[str, bytes]:
"""Split a base64 data URL into ``(mime_type, raw_bytes)``.
Raises:
ValueError: if ``data_url`` is not a data URL or its base64 payload is
malformed.
"""
if not is_data_url(data_url):
raise ValueError("Not a base64 data URL")
header, _, payload = data_url.partition(",")
mime_type = header[len(_DATA_URL_PREFIX):].split(";", 1)[0].strip() or "image/jpeg"
try:
raw = base64.b64decode(payload, validate=True)
except (binascii.Error, ValueError) as exc:
raise ValueError(f"Invalid base64 data URL payload: {exc}") from exc
return mime_type, raw
def image_url_part(url: str) -> dict[str, Any]:
"""Build an ``image_url`` content part from a URL or inline data URL."""
return {"type": "image_url", "image_url": {"url": url}}
def image_part_from_bytes(data: bytes, mime_type: str = "image/jpeg") -> dict[str, Any]:
"""Build an inline base64 ``image_url`` content part from raw bytes."""
return image_url_part(encode_image_data_url(data, mime_type))
def image_part_from_path(file_path: str | Path) -> dict[str, Any]:
"""Read a local image file and build an inline base64 ``image_url`` part."""
path = Path(file_path)
mime_type = mimetypes.guess_type(path.name)[0] or "image/jpeg"
return image_part_from_bytes(path.read_bytes(), mime_type)