# 读取图片、PDF 和文本文件 (/docs/cloud/services/model/guides/multimodal)



先确认模型同时支持所选协议和输入类型。查看 `GET /model/v1/models` 的 `supported_apis`、`architecture.input_modalities` 和 `x_praxis.input_modalities`：`native` 表示原生输入，`converted` 表示经过转换，`unsupported` 表示不可用。支持图片不代表支持音频或视频。

本例用 `deepseek-flash` 读取内联图片，用 `qwen3.8-flash` 读取 PDF 和文本的 `file_id` 引用；后者声明原生 PDF 输入。文件上传和生命周期细节见[文件服务指南](/docs/cloud/services/file)。

## 1. 准备终端 [#1-准备终端]

准备 Python 3.10+ 和 [uv](https://docs.astral.sh/uv/getting-started/installation/)。将 [Praxis 云 API Key](/docs/cloud/account/api-keys) 放入环境变量 `PRAXIS_API_KEY`，不要写入代码或日志。

```bash
test -n "$PRAXIS_API_KEY"
```

期望：退出码为 0。退出码 1 表示变量未设置，先设置再继续。示例默认使用 `https://api.deepraxis.ai`。切换环境时，将 `PRAXIS_API_BASE` 设为相应 API 域名，不带末尾斜杠或 API 路径。

## 2. 发送已知内容并校验回答 [#2-发送已知内容并校验回答]

脚本在本地构造红色 PNG、单页 PDF 和文本文件。先校验图片颜色，再以 `purpose=user_data` 上传两个文档并询问校验码。每个上传文件都在 `finally` 中删除，包括回答检查失败的情况，无需自备文件。

保存为 `multimodal.py`，运行 `uv run -q --no-project --with openai --with anthropic --with google-genai python multimodal.py`。

```python
import base64
import os
import struct
import zlib
from openai import OpenAI

client = OpenAI(api_key=os.environ['PRAXIS_API_KEY'],
    base_url=os.environ.get('PRAXIS_API_BASE', 'https://api.deepraxis.ai') + '/openai/v1')

# Build a 128 x 128 red PNG using only the Python standard library.
def chunk(kind, data):
    return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data))
image = (b'\x89PNG\r\n\x1a\n'
    + chunk(b'IHDR', struct.pack('>IIBBBBB', 128, 128, 8, 2, 0, 0, 0))
    + chunk(b'IDAT', zlib.compress((b'\x00' + b'\xff\x00\x00' * 128) * 128))
    + chunk(b'IEND', b''))
response = client.chat.completions.create(model='deepseek-flash', reasoning_effort='none', max_tokens=256,
    messages=[{'role': 'user', 'content': [
        {'type': 'text', 'text': 'Name the main color in this image in English.'},
        {'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,' + base64.b64encode(image).decode()}},
    ]}])
print('X-Request-Id:', response._request_id)
answer = response.choices[0].message.content or ''
assert response.choices[0].finish_reason == 'stop' and 'red' in answer.lower(), answer
print('image:', answer)

# Build a one-page PDF with selectable text.
text = b'The verification code is LIME-742.'
stream = b'BT /F1 18 Tf 50 100 Td (' + text + b') Tj ET'
objects = [b'<< /Type /Catalog /Pages 2 0 R >>',
    b'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
    b'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 400 200] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>',
    b'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>',
    b'<< /Length ' + str(len(stream)).encode() + b' >>\nstream\n' + stream + b'\nendstream']
pdf, offsets = b'%PDF-1.4\n', [0]
for index, obj in enumerate(objects, 1):
    offsets.append(len(pdf))
    pdf += f'{index} 0 obj\n'.encode() + obj + b'\nendobj\n'
xref = len(pdf)
pdf += b'xref\n0 6\n0000000000 65535 f \n'
pdf += b''.join(f'{offset:010d} 00000 n \n'.encode() for offset in offsets[1:])
pdf += f'trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n'.encode()

for name, data, mime in [('code.pdf', pdf, 'application/pdf'), ('code.txt', text, 'text/plain')]:
    file = client.files.create(file=(name, data, mime), purpose='user_data')
    print('upload X-Request-Id:', file._request_id)
    try:
        response = client.chat.completions.create(model='qwen3.8-flash', reasoning_effort='none', max_tokens=256,
            messages=[{'role': 'user', 'content': [
                {'type': 'text', 'text': 'What is the verification code in the file? Reply with only the code.'},
                {'type': 'file', 'file': {'file_id': file.id}},
            ]}])
        print('chat X-Request-Id:', response._request_id)
        answer = response.choices[0].message.content or ''
        assert response.choices[0].finish_reason == 'stop' and 'LIME-742' in answer, answer
        print(name, answer)
    finally:
        deleted = client.files.delete(file.id)
        print('delete X-Request-Id:', deleted._request_id)
        assert deleted.deleted
```

期望：图片回答包含 `red`，`code.pdf` 和 `code.txt` 都返回 `LIME-742`，两个删除操作成功，退出码 0。仅 HTTP 成功不够，内容断言也必须通过。

## 实测失败与处理 [#实测失败与处理]

同一 PDF 使用 `deepseek-flash` 时，HTTP 200，但回答说：

> I wasn't able to read the attached PDF

该模型目录标注 PDF 为转换输入，这次未能证明成功读取。将文档请求切到原生 PDF 输入的 `qwen3.8-flash` 后，内容校验通过。遇到无法读取时，选已验证模型或直接提供相关文本；不要仅凭回答看似合理就认为文件已读到。请求结束前保持文件可用。

本页验证的 `file_id` 路径接受 PDF 和文本，不限于图片；这不代表所有模型支持所有文件类型。参数见 [Chat Completions](/docs/api/compatible/openai/createChatCompletion)。

[API 参考 →](/docs/api/compatible/openai) · [Cookbook →](/docs/cookbook)
