读取图片、PDF 和文本文件
选择输入类型,发送内容,并验证模型实际读到了什么。
先确认模型同时支持所选协议和输入类型。查看 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 输入。文件上传和生命周期细节见文件服务指南。
1. 准备终端
准备 Python 3.10+ 和 uv。将 Praxis 云 API Key 放入环境变量 PRAXIS_API_KEY,不要写入代码或日志。
test -n "$PRAXIS_API_KEY"期望:退出码为 0。退出码 1 表示变量未设置,先设置再继续。示例默认使用 https://api.deepraxis.ai。切换环境时,将 PRAXIS_API_BASE 设为相应 API 域名,不带末尾斜杠或 API 路径。
2. 发送已知内容并校验回答
脚本在本地构造红色 PNG、单页 PDF 和文本文件。先校验图片颜色,再以 purpose=user_data 上传两个文档并询问校验码。每个上传文件都在 finally 中删除,包括回答检查失败的情况,无需自备文件。
保存为 multimodal.py,运行 uv run -q --no-project --with openai --with anthropic --with google-genai python multimodal.py。
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。