Read images, PDFs, and text files

Select an input modality, send the content, and verify what the model read.

Choose a model that supports both your protocol and input type. Check supported_apis, architecture.input_modalities, and x_praxis.input_modalities in GET /model/v1/models. native means native input, converted means conversion, and unsupported means unavailable. An image-capable model is not automatically an audio- or video-capable model.

This example uses deepseek-flash for an inline image and qwen3.8-flash for PDF and text file_id references. The latter declares native PDF input. File upload and lifecycle details belong to the file service guide.

1. Prepare your terminal

Use Python 3.10+ and uv. Put your Praxis cloud API Key in PRAXIS_API_KEY; do not put it in source code or logs.

test -n "$PRAXIS_API_KEY"

Expected: exit code 0. Exit code 1 means the variable is missing. Examples default to https://api.deepraxis.com. To switch environments, set PRAXIS_API_BASE to the API origin without a trailing slash or API path.

2. Send known content and verify the answer

The script creates a red PNG, a one-page PDF, and a text file locally. It verifies the color, then uploads both documents with purpose=user_data and asks for their verification code. Each uploaded file is deleted in finally, including when a content check fails. No existing files are required.

Save as multimodal.py and run 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.com') + '/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

Expected: the image answer contains red; both code.pdf and code.txt return LIME-742; both deletions succeed. Exit code 0. HTTP success alone is insufficient: the content assertions must pass.

Observed failure and recovery

With the same PDF, deepseek-flash returned HTTP 200 but said:

I wasn't able to read the attached PDF

Its catalog entry advertises converted PDF input; that did not establish successful reading in this test. Switching the document calls to native-PDF qwen3.8-flash passed the content checks. If a model cannot read the document, use a verified model or supply the relevant text directly. Do not accept a plausible answer without checking known content. Keep the file available until the request finishes.

For this tested path, file_id accepts PDF and text files, not just images. This does not imply every model supports every file type. Parameters: Chat Completions.

API reference → · Cookbook →

Export

On this page