Check Which Models Read Images
Build a helper that finds image-capable models and chains a captioner.
Introduction
30 Second Summary
Two AI models from the same family can react very differently to the same picture. A failed image question can leave you guessing whether the picture or the model caused the problem.
In this project, you will use the OpenRouter model catalog to discover which model slugs accept images. You will test a screenshot before building a captioning chain that lets a text-only model answer a question about that image.
What You'll Build
You will run a script that turns one screenshot into a final answer from a text-only model while showing every proof along the way.
By the end of this project, you'll have:
- A readable model comparison showing every slug's input modalities. Image-capable options are clearly flagged.
- A real screenshot test that exposes the text-only model's error. The same test produces a description from a vision model.
- A working captioner chain that gives a text-only model enough context to answer a question about an image it never received.
- Secret Mission: An optional challenge to push your skills further.
Are there any prerequisites?
You need access to a terminal.
You need a screenshot to test.
You also need an OpenRouter API key with a small balance.
Before We Start
A missing interpreter can look like an API failure. An unset credential can cause the same confusion.
In this step, you'll verify Python 3. You'll prepare access to OpenRouter. You'll also save the image that tests each model's input capabilities.
In this step, get ready to:
- Verify Python 3 from your terminal.
- Create an OpenRouter API key.
- Save a local image as screenshot.png.
Verify Python 3 in your terminal
The terminal runs the scripts that inspect model modalities. Checking Python first keeps interpreter problems separate from API problems.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal on macOS or PowerShell on Windows.
- Press Enter to open the terminal.
macOS
- Check your Python version by running this command:
python3 --version
What does this command do?
The --version option prints the version attached to python3. The output confirms whether your terminal can start Python 3.
Windows
- Check your Python version by running this command:
python --version
What does this command do?
The --version option prints the version attached to python. The output confirms whether PowerShell can start Python 3.
✔️ I see Python 3
Your Python interpreter is ready. You can now run the scripts that compare model modalities.
ⓧ I see Python 2
The command is reaching Python 2. This project needs Python 3.
- Visit the official Python download page.
- Download the current Python 3 installer for your operating system.
- Run the installer.
- Follow the installation prompts.
- Close your terminal after the installation finishes.
- Return to the terminal through your operating system's search bar.
- Repeat the version check from the platform tab above.
You'll see output beginning with Python 3.
ⓧ Command not found
Your terminal cannot find a Python interpreter yet. Installing Python adds the command needed for this project.
- Visit the official Python download page.
- Download the current Python 3 installer for your operating system.
- Run the installer.
- Follow the installation prompts.
- Close your terminal after the installation finishes.
- Return to the terminal through your operating system's search bar.
- Repeat the version check from the platform tab above.
You'll see output beginning with Python 3.
Still unable to run Python?
A terminal opened before installation may still use the old command path. Close it fully before opening a new session.
If the command still fails, help me troubleshoot my Python installation.
Create your OpenRouter key
An API key authorizes requests from your account. An environment variable keeps that credential outside the scripts you build.
Adding credits is the only billing action in this setup. Keep your balance low so these short requests have a firm spending boundary.
- Visit the OpenRouter Credits page.
- Add a small credit balance.
- Visit the OpenRouter API Keys page.
- Sign up or log in if prompted.
- Select Create API Key.
- Enter image-modality-check as the key name.
- Set a small Credit limit that fits your account balance.
- Create the key.
Finding the generated value is a little fiddly because OpenRouter shows it only once. Copy it before closing the key dialog.
Protect your API key
Treat the copied value like a password. Keep it out of screenshots.
If the key becomes exposed, delete it from your OpenRouter key settings. Create a replacement before making another request.
OpenRouter uses OPENROUTER_API_KEY as the environment variable name. Set it in the terminal you already opened.
macOS
- Replace sk-or-v1-YOUR-API-KEY in the command below with the key you copied.
- Set the environment variable for this terminal session by running:
export OPENROUTER_API_KEY="sk-or-v1-YOUR-API-KEY"
What does this command do?
The command stores your key under OPENROUTER_API_KEY in the current terminal session. Your Python scripts can read the credential without placing it in a source file.
The terminal returns to its prompt without printing the key. That quiet result keeps the credential off your screen.
Windows
- Replace sk-or-v1-YOUR-API-KEY in the command below with the key you copied.
- Set the environment variable for this PowerShell session by running:
$env:OPENROUTER_API_KEY="sk-or-v1-YOUR-API-KEY"
What does this command do?
The command stores your key under OPENROUTER_API_KEY in the current PowerShell session. Your Python scripts can read the credential without placing it in a source file.
PowerShell returns to its prompt without printing the key. That quiet result keeps the credential off your screen.
- Keep this terminal window open for the next step.
Terminal rejected the key command?
Confirm that you used the command from the tab matching your operating system. Check that you kept the quotation marks around the key.
Still stuck? Help me set the OpenRouter environment variable safely.
Save your test image as screenshot.png
Both model tests need the same image. A fixed filename makes the selected model slug the only changing input.
Choose a useful screenshot
Capture a screen with several visible details that a model could describe. Avoid passwords or personal information.
Save the image in the PNG format as screenshot.png. The text-only test uses this exact file before the vision-capable test uses it again.
macOS
- Display a non-sensitive screen with several visible details.
- Press Shift-Command-4 to start a selected-area screenshot.
- Drag the crosshair over the area you want to capture.
- Release your mouse or trackpad button to take the screenshot.
- Wait for the PNG file to save to your Desktop.
- Select the newest PNG file on your Desktop in Finder.
- Press Return to make the filename editable.
- Enter screenshot.png as the new filename.
- Press Return to save the new filename.
Windows
- Display a non-sensitive screen with several visible details.
- Press Windows logo key + Shift + S to open the Snipping Tool overlay.
- Drag over the area you want to capture.
- Select the capture notification to open the image in Snipping Tool.
- Select Save as.
- Choose your Desktop as the save location.
- Enter screenshot.png as the filename.
- Select Save.
Before you check, what filename do you expect to see on your Desktop?
- Open your Desktop in Finder on macOS or File Explorer on Windows.
- Open screenshot.png.
You'll see the screen area you captured. Your Desktop now contains a working image named screenshot.png.
Image missing or named incorrectly?
Look for the newest PNG file in your Desktop folder. Rename that file to screenshot.png if the screenshot tool used a timestamp.
If the file does not open as an image, take the screenshot again. Help me prepare screenshot.png.
Your terminal can run Python. Your OpenRouter key is available to the current session. Next up, you'll fetch the model catalog to find the slugs in one model family.
Fetch Your Model Family
A model family can contain several slugs with different capabilities. The family name alone cannot tell you which records belong in an image workflow.
In this step, you will request the OpenRouter model catalog with your API key. Your script will keep every record whose slug belongs to the DeepSeek V4 family.
In this step, get ready to:
- Create the model catalog script beside your screenshot.
- Fetch the authenticated model catalog.
- Filter the catalog to the configured model family.
Connect to the model catalog
The model catalog returns complete records for the available slugs. Each record stays intact so later steps can inspect fields such as its input modalities.
- Use a plain-text code editor to create model_modality_demo.py in the same folder as screenshot.png.
- Add the authenticated catalog request plus the family filter by pasting this code into model_modality_demo.py:
import json
import os
from urllib.request import Request, urlopen
MODELS_URL = "https://openrouter.ai/api/v1/models"
MODEL_FAMILY = "deepseek/deepseek-v4"
# Read the existing key without placing the secret in source code.
api_key = os.environ["OPENROUTER_API_KEY"]
# Fetch the catalog with the key in the authorization header.
request = Request(
MODELS_URL,
headers={"Authorization": f"Bearer {api_key}"},
)
with urlopen(request) as response:
catalog = json.load(response)
# Keep the complete records for slugs in the selected family.
models = catalog["data"]
family_models = [
model for model in models
if model["id"].startswith(MODEL_FAMILY)
]
print(f"Found {len(family_models)} models in {MODEL_FAMILY}.")
What Does This Code Do?
- The MODELS_URL value points to the catalog endpoint.
- The MODEL_FAMILY value defines the slug prefix your script keeps.
- The Request object sends your environment-variable key in the authorization header.
- The family_models list retains each complete matching record for later inspection.
Run the family filter
The script prints a count after the catalog request succeeds. A non-zero count confirms that authentication worked plus the configured prefix matched catalog records.
- Save model_modality_demo.py.
Before you run the script, do you expect the configured family to match zero records or several records?
- Fetch the catalog and test the filter by running this command:
python3 model_modality_demo.py
What Should This Prove?
You should see Found followed by a non-zero number plus the deepseek/deepseek-v4 family prefix. This proves the script fetched the catalog and retained the matching records.
That is your first catalog check working. Your script now has the exact family records that the next modality check needs.
Catalog Request Did Not Work?
- Confirm that you are running the script from the folder containing model_modality_demo.py.
- Confirm that OPENROUTER_API_KEY is still set in the current terminal session.
- Check that MODEL_FAMILY still contains deepseek/deepseek-v4 if the script reports zero matches.
Still stuck? Help me troubleshoot the model catalog request.
The finished file now contains the complete fetch plus filter path.
✔️ Awesome, I've got everything!
Great. Double-check that model_modality_demo.py is saved beside screenshot.png.
ⓧ I'd like to double check the full code
import json
import os
from urllib.request import Request, urlopen
MODELS_URL = "https://openrouter.ai/api/v1/models"
MODEL_FAMILY = "deepseek/deepseek-v4"
# Read the existing key without placing the secret in source code.
api_key = os.environ["OPENROUTER_API_KEY"]
# Fetch the catalog with the key in the authorization header.
request = Request(
MODELS_URL,
headers={"Authorization": f"Bearer {api_key}"},
)
with urlopen(request) as response:
catalog = json.load(response)
# Keep the complete records for slugs in the selected family.
models = catalog["data"]
family_models = [
model for model in models
if model["id"].startswith(MODEL_FAMILY)
]
print(f"Found {len(family_models)} models in {MODEL_FAMILY}.")
Your selected family is now separated from the rest of the catalog. Next up, you will inspect each retained slug and reveal which ones accept images.
Identify Image-Capable Slugs
Your script already narrows the OpenRouter model catalog to one family. You now need evidence about what each matching slug accepts.
Slugs in the same family can declare different input modalities. This step turns that metadata into a readable table for choosing one text-only slug plus one image-capable slug.
In this step, get ready to:
- Print every matching slug with its declared input modalities.
- Mark the slugs that accept image input.
- Configure one text-only slug plus one image-capable slug.
Print each slug and its input modalities
Each catalog record includes an identifier for API requests. Its nested architecture metadata lists the input types that the slug accepts.
How does the catalog show image support?
- The id field contains the model slug used in requests.
- The architecture object contains the model's capability metadata.
- The input_modalities list contains the input types accepted by that slug.
- The value image confirms that the slug accepts image input.
- Open model_modality_demo.py in the editor you used earlier.
- Locate the section that retains records matching MODEL_FAMILY.
- Extend that section so every retained record prints its id with its nested input_modalities list.
- Save model_modality_demo.py.
Before you run the script, do you expect every matching slug to declare the same input types?
- Run model_modality_demo.py with the same Python 3 command you used earlier.
You should see one row for every matching slug. Each row shows the slug plus its complete input-modality list.
- Return to model_modality_demo.py in your editor.
- Add a visible marker when a record's input_modalities list contains image.
- Save model_modality_demo.py.
Before you run the script again, which rows do you expect to gain the marker?
- Run model_modality_demo.py again with your Python 3 command.
You should see the same family rows with a marker beside every slug whose modalities contain image.
That is the capability check working. Your table now exposes the difference between related slugs before you send an image.
Missing slugs or modality labels?
- Confirm that MODEL_FAMILY still contains the family value configured earlier.
- Confirm that your output reads input_modalities from the nested architecture object.
- Confirm that your marker checks for the exact value image inside the modalities list.
Still stuck? Help me debug my modality table.
Choose a text-only and image-capable pair
Your two selected slugs create a controlled comparison. One excludes image input while the other explicitly includes it.
- Compare the printed rows to find a slug whose modalities exclude image.
- Record that slug as your text-only model slug.
- Find a marked slug whose modalities contain image.
- Record that slug as your image-capable model slug.
Why choose from the table?
A family label groups related models. The modalities attached to each slug provide the request-specific evidence.
Reading the live catalog keeps your choices tied to the capabilities currently declared by the API.
Configure and verify the selected slugs
The final configuration assigns a clear role to each choice. TEXT_ONLY_SLUG becomes the comparison model while VISION_SLUG becomes the model that can receive the screenshot.
- In model_modality_demo.py, add a selected-model configuration beside MODEL_FAMILY using your text-only model slug for TEXT_ONLY_SLUG plus your image-capable model slug for VISION_SLUG.
- Save model_modality_demo.py.
Before the final run, do you expect each configured slug to match its intended capability?
- Run model_modality_demo.py again with your Python 3 command.
You should see every matching slug with its input modalities. The row configured as VISION_SLUG carries the image-capable marker.
- Compare TEXT_ONLY_SLUG with its printed row to confirm that image is absent.
- Compare VISION_SLUG with its printed row to confirm that image is present.
You now have catalog evidence behind both model choices. The script can distinguish image-capable slugs before a request reaches the model.
Your model pair is configured for a direct capability test. Next, you will submit screenshot.png to the text-only slug and inspect the response.
Test the Text-Only Model
Your catalog table already shows which slugs declare image input. A real request now tests whether that metadata predicts the model's behavior.
In this step, you'll submit screenshot.png to TEXT_ONLY_SLUG through the model API. You'll capture the response so the result remains visible in your terminal.
In this step, get ready to:
- Add an image-message test for TEXT_ONLY_SLUG.
- Run the test to inspect the API response.
Add the image request
A useful boundary test keeps its input constant. The vision model uses this same screenshot in the next step.
What belongs in the test?
- The request uses TEXT_ONLY_SLUG as its model.
- The message submits screenshot.png as image content.
- Error handling captures the rejected response.
- The terminal output preserves the response for inspection.
- Switch back to model_modality_demo.py from earlier.
- Extend the code immediately after the modality table with the image-message test described above.
- Save model_modality_demo.py.
Run the text-only test
The catalog declaration gives you a prediction about model behavior. This request checks that prediction against the live service.
Before you run it, do you think the selected slug accepts the screenshot or rejects the image content?
- Switch back to the terminal from earlier.
- Run the updated script with Python 3 by running this command:
python3 model_modality_demo.py
What does this command do?
- Python 3 executes the saved model_modality_demo.py file.
- The script prints the filtered modality table before reaching the image test.
- The final response records how the API handles the image request.
You'll see the filtered modality table first. The script then prints an API error response indicating that the selected slug does not accept image input.
You have now turned the catalog metadata into a real boundary test. The terminal output proves why checking modalities before a request matters.
Got a different result?
- Confirm that screenshot.png is beside model_modality_demo.py if the script stops before making the request.
- Return to the authenticated terminal session from earlier if the response mentions an authentication problem.
- Return to the modality table if the image request succeeds. Confirm that TEXT_ONLY_SLUG comes from a row without image input.
Still stuck? Help me diagnose why my text-only model test did not print the expected unsupported-image response.
Your text-only boundary is now proven with a real request. Next, you'll send the same screenshot to the configured vision model and read its description.
Describe the Screenshot with Vision
Your last request turned the catalog's input modalities into proof. TEXT_ONLY_SLUG rejected screenshot.png because that slug cannot accept images.
The same model family also contains VISION_SLUG. In this step, you'll use it to turn the same screenshot into image_description.
In this step, get ready to:
- Reuse the existing image message with VISION_SLUG.
- Capture the vision model's returned text in image_description.
- Run the script to confirm the description matches the screenshot.
Trace the existing image request
A fair comparison changes one factor at a time. Here, that factor is the model slug.
- Switch back to model_modality_demo.py from the previous step.
- Find the request that submits screenshot.png to TEXT_ONLY_SLUG.
- Identify the point immediately after the unsupported-image error is printed.
Why reuse the same payload?
Reusing the same screenshot controls the input. The selected slug becomes the only meaningful change.
That comparison turns the catalog metadata into observed behavior.
Add the vision request
A successful vision response contains assistant text that describes the picture. Saving that text under a clear name prepares it for the handoff in the next step.
What the new request must do
- The model selector is VISION_SLUG.
- The image message reuses the existing screenshot.png payload.
- The returned assistant text is stored in image_description.
- The script prints image_description after the response arrives.
- Add the complete vision request beneath the existing error handling using the text-only request as your template.
- Save model_modality_demo.py.
Run the vision check
Before the rerun, the question is whether the unchanged image payload produces another modality error or a description.
- Rerun model_modality_demo.py using the same terminal command from the previous step.
You'll see the filtered modality table first. The text-only request still prints its unsupported-image error.
The vision request then prints a natural-language description from image_description. The description should name visible details from screenshot.png.
You have now proved that an image-capable slug can read the same payload that the text-only slug rejects.
Description missing or incorrect?
- Check that the second request uses VISION_SLUG if it also reports unsupported image input.
- Compare the VISION_SLUG catalog row with the image-capable marker from earlier.
- Confirm that image_description receives the returned assistant text before it is printed.
- Confirm that the vision request reuses the existing screenshot.png payload.
For more help, trace my vision request with me.
- Compare your finished flow with the checkpoint tabs below.
✔️ Awesome, I've got everything!
Your script still prints the filtered catalog modalities.
It still demonstrates the text-only image error. It now prints image_description from VISION_SLUG.
ⓧ I'd like to double check the full code
- Confirm that MODEL_FAMILY, TEXT_ONLY_SLUG, and VISION_SLUG remain configured.
- Confirm that the filtered catalog modalities still print before the requests.
- Confirm that the text-only request still submits screenshot.png.
- Confirm that the text-only error response still prints.
- Confirm that the vision request submits the same screenshot.png payload to VISION_SLUG.
- Confirm that the returned visual description is assigned to image_description.
- Confirm that image_description is printed.
Your vision model now turns screenshot.png into image_description. Next up, you'll pass that description to TEXT_ONLY_SLUG as ordinary text.
Pass the Description to Text
Your vision-capable slug now turns screenshot.png into image_description. That text gives the final stage something it can consume.
In this step, you'll finish a model chain. TEXT_ONLY_SLUG receives image_description with QUESTION. The script prints its answer about a screenshot it never saw.
In this step, get ready to:
- Define the question you want answered about the screenshot.
- Pass the visual description to the text-only model.
- Prove the chain works through the final printed answer.
Build the text handoff
QUESTION holds one clear question about a detail in screenshot.png. The final request targets TEXT_ONLY_SLUG.
Its message contains image_description plus QUESTION as ordinary text. The script prints the returned answer.
- Extend model_modality_demo.py with QUESTION plus the final handoff described above.
- Save model_modality_demo.py.
What Does the Handoff Preserve?
The text-only model can reason only over details preserved in image_description. Any visual detail omitted by VISION_SLUG is absent from the final answer.
✔️ Awesome, I've got everything!
Your full file now connects the vision model's description to the text-only model as ordinary text.
ⓧ I'd like to double check the full code
- Confirm QUESTION is defined alongside MODEL_FAMILY, TEXT_ONLY_SLUG, and VISION_SLUG.
- Confirm the final request appears after image_description has been stored.
- Confirm the final request targets TEXT_ONLY_SLUG.
- Confirm the final request contains image_description plus QUESTION as text.
- Confirm the script prints the text returned by the final request.
Run the complete chain
A successful run proves that each model stays within its supported input modalities. The vision model sees the image while the text-only model sees only text.
Before you run it, do you think the text-only model can answer accurately using only the description?
- Switch back to the terminal from earlier.
- Run model_modality_demo.py again with the same Python 3 command you used earlier.
You'll see the familiar modality table first. The expected text-only image error follows.
Next, you'll see the vision model's description. The final printed section contains TEXT_ONLY_SLUG's answer to QUESTION.
You have closed the loop. Your text-only model now answers a question about an image through text alone.
No Final Answer?
- Confirm QUESTION is defined before the final request runs.
- Check that the final request contains the stored image_description.
- Make sure the final request uses TEXT_ONLY_SLUG.
Still stuck? Help me trace the description through my model chain.
Secret mission
Audit the Caption Handoff
A working captioner chain can still drop the exact detail a question needs. Stress-test the handoff with a precise visual question. Inspect what survives in image_description before restoring your reusable helper.
Clean Up Your Resources
Clean Up Your Resources
The model API calls you made may have used some of your account balance. Keeping the local files creates no ongoing charges.
Resources you used:
- The reusable model_modality_demo.py script.
- The local screenshot.png image.
Keep everything running
No action is needed. This option preserves the modality helper for future model checks.
- Keep model_modality_demo.py so you can check input modalities before sending another image.
- Keep screenshot.png if you want to use the same test image for future comparisons.
- Leave your model API key outside the script.
Pause - I'll come back to this later
No service continues running after the script finishes. Your local files can stay in place without using more account balance.
- Close the terminal session from earlier.
- Leave model_modality_demo.py in its current folder.
- Leave screenshot.png in the same folder.
- Return to the files when you want to test another model family.
Delete - I don't want to use this again
Complete removal means deleting the two local files. Your model API account stays unchanged.
- Use your operating system's file search to find model_modality_demo.py.
- Open the folder that contains the matching file.
- Delete model_modality_demo.py.
- Delete screenshot.png from the same folder.
- Empty the location where your operating system stores deleted files.
- Refresh the folder.
That clears the local footprint. You should see neither project file listed.
Nice Work!
Nice Work!
You made it! Your Python script now checks the OpenRouter catalog to identify each slug's input modalities. It also lets a text-only model answer questions about a screenshot through a vision model's description.
You've learned how to:
- Filtered one model family into a readable modality table that marks image-capable slugs.
- Sent screenshot.png to TEXT_ONLY_SLUG to prove the unsupported image path. Read the returned error as evidence of a modality mismatch.
- Built a vision-to-text chain that creates image_description from screenshot.png. Passed that description to TEXT_ONLY_SLUG to get an answer about an image it never received.
- Completed an optional Secret Mission to push your model modality skills further.
Ready to quiz yourself?