Integration guides · 2026-10-22

Reading scientific charts and tables with the Intern-S2 API

Send a scientific chart or table image to Intern-S2 as base64 and get the rows back as JSON; the model is free until 31 October 2026.

Diagram: one box under https://llmtr.com/v1, internlm/intern-s2-free; a table image goes in on the left and JSON rows come out on the right.

Short answer: send the image to Intern-S2 and ask for JSON in the prompt

LLMTR is a gateway that gives access to language models from different providers through one OpenAI-compatible API. If you need to read a scientific chart or table from an image, call Intern-S2 on LLMTR with the model id internlm/intern-s2-free on the /v1/chat/completions endpoint at https://llmtr.com/v1; the model accepts images and is free until 31 October 2026, 23:59 (TRT). The image goes in the message as an image_url part, next to a text part with your question. After 31 October 2026, use nvidia/nemotron-3.5-super-vl-free as the free row and qwen/qwen3.6-35b-a3b as the paid row for the same job.

Shanghai AI Lab's model card describes Intern-S2 as a multimodal foundation model for scientific intelligence and long-horizon agents. According to the card, the model learns directly from raw pages of scientific literature through visual pretraining and was trained with scientific reinforcement-learning tasks across more than 20 domains; its license is Apache 2.0. The LLMTR model page also states that it can answer questions about figures, charts and tables in an image.

How do you read a scientific chart or table from an image with an API?

Open an account on llmtr.com, create a key under Dashboard > API Keys and put it in the LLMTR_API_KEY environment variable. Set the base_url of the OpenAI client to https://llmtr.com/v1. If the image sits at an https address, you can put that address in the image_url part; if the file is on your local disk, read it and encode it as base64 as in the example below, then build a data URL with the data:image/png;base64, prefix.

Intern-S2 has no JSON mode, so describe the format in the prompt: one object per row, the column headers as keys, and numbers and units as printed in the image. The example tries to parse the answer with json.loads; if the model wrapped the JSON in a code block, it removes those marks first, and if parsing fails, it prints the start of the answer. It also catches a 429 and shows the Retry-After header.

Always check finish_reason. stop means the answer ended on its own; length means it was cut off at the max_tokens limit. A truncated JSON array cannot be parsed, so when you see length, raise max_tokens or split the table into parts and ask for fewer rows per request. The example sets max_tokens to 8192 because reasoning tokens count toward that limit too.

OpenAI Python SDK example that sends a local table image to internlm/intern-s2-free as a base64 data URL, asks for the rows as JSON in the prompt, parses the answer with json.loads and prints finish_reason and usage.

import base64
import json
import os

from openai import OpenAI, RateLimitError

client = OpenAI(base_url="https://llmtr.com/v1", api_key=os.environ["LLMTR_API_KEY"])

# Until 31 October 2026, 23:59 (TRT). From 1 November 2026 this id returns 410:
# switch to "nvidia/nemotron-3.5-super-vl-free" or the paid "qwen/qwen3.6-35b-a3b".
MODEL = "internlm/intern-s2-free"

# Read a local PNG and send it as a base64 data URL.
with open("table.png", "rb") as f:
    image_url = "data:image/png;base64," + base64.b64encode(f.read()).decode("ascii")

# No JSON mode on this row: describe the JSON you want in the prompt.
prompt = (
    "Read the table in this image. Return only a JSON array with one object per row, "
    "using the column headers as keys. Copy numbers and units exactly as printed."
)

try:
    response = client.chat.completions.create(
        model=MODEL,
        messages=[{
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {"type": "image_url", "image_url": {"url": image_url}},
            ],
        }],
        # Reasoning is on by default and its tokens count toward max_tokens.
        # For a plain transcription you can turn it off: extra_body={"reasoning": False}
        max_tokens=8192,
    )
except RateLimitError as err:
    print("Limit reached, Retry-After:", err.response.headers.get("retry-after"))
    raise SystemExit(1)

choice = response.choices[0]
if choice.finish_reason == "length":
    print("Cut off at max_tokens: raise it or ask for fewer rows per request.")

text = (choice.message.content or "").strip()
if text.startswith("```"):
    # Remove a Markdown code fence if the model added one.
    text = text.strip("`").removeprefix("json").strip()
try:
    rows = json.loads(text)
    print(len(rows), "rows:", rows[:3])
except json.JSONDecodeError:
    print("Not valid JSON, first 300 characters:", text[:300])

print(response.usage)

Request details on Intern-S2: images, JSON and tools

Do not send response_format; this row does not support JSON mode. If you define tools, use auto for tool_choice: required or a named function returns 400, and none also returns 400 while tools are attached. Without tools, leave out the tools field entirely.

LLMTR's image input docs recommend JPEG or PNG images under 5 MB with sides between 512 and 2048 pixels; when the image carries text to read, they ask for high contrast and a sharp resolution. The image counts toward the context and the daily allowance; compare usage.prompt_tokens with and without it to see its share.

Intern-S2 on LLMTR as of 22 October 2026
PropertyIntern-S2
Model idinternlm/intern-s2-free
Endpoint/v1/chat/completions (https://llmtr.com/v1)
Context window262,144 tokens
Output per responseno separate limit listed, bounded by the context window
Inputtext and images (an https address or a base64 data URL); video_url returns 400
JSONresponse_format not supported; ask for JSON in the prompt
tool_choiceauto only; required and a named function return 400, none returns 400 while tools are attached
Reasoningon by default, arrives in reasoning_content; turned off with "reasoning": false or :fast; reasoning_effort returns 400
Period and daily allowanceuntil 31 October 2026, 23:59 (TRT); 500,000 tokens a day per model (input and output combined), new accounts start with a smaller allowance on their first day
From 1 November 2026410 model_retired; the request is not redirected to another model

Long tables: leave reasoning on or turn it off?

Intern-S2 reasons before answering by default; the reasoning text arrives in the reasoning_content field and the answer in content. Reasoning tokens count toward the max_tokens limit and toward the daily allowance. The allowance is 500,000 tokens a day per model, counts input and output combined and renews every day at 00:00 (TRT); new accounts start with a smaller allowance on their first day. A reading request of 8,000 tokens, input and output combined, fits into that allowance 62 times a day. When the allowance runs out, the request returns 429 rate_limit_error, and the Retry-After header tells you when to try again.

The choice depends on the job. For a long transcription that copies cells as they are, turn reasoning off: send "reasoning": false (in Python, extra_body={"reasoning": False}) or add :fast to the end of the model id. More of max_tokens is then left for the rows. For a trend in a chart, the gap between two series or a value read off an axis and then calculated, leave reasoning on and keep max_tokens generous; :think or "reasoning": true keeps it on. reasoning_effort returns 400 on this row, as reasoning has no levels.

Which one when?

The choices below follow from what each row accepts, its JSON support, its reasoning setting and its date; compare reading accuracy with your own figures and tables.

  • Reading a scientific figure, chart or table from an image at no cost until 31 October 2026, 23:59 (TRT): Intern-S2.
  • A long table transcription that copies cells as printed: Intern-S2 with reasoning turned off through :fast or "reasoning": false.
  • A question that asks for a trend, a difference or a value calculated from an axis: Intern-S2 with reasoning on, a generous max_tokens and a check on finish_reason.
  • Output enforced with JSON Schema, video, or keeping going free of charge after 31 October 2026: Nemotron 3.5 Super VL; for a paid row, Qwen3.6 35B-A3B.
  • An image with sensitive, personal or customer data: do not send it to the rows in this article; the data notice of Intern-S2 and Nemotron 3.5 Super VL says prompts may be logged and used to improve the model.

Which row replaces Intern-S2 from 1 November 2026?

From 1 November 2026, 00:00 (TRT), requests to Intern-S2 return 410 model_retired and are not redirected to another model; you have to change the model id yourself. To keep going free of charge after 31 October 2026, use Nemotron 3.5 Super VL with the id nvidia/nemotron-3.5-super-vl-free: it accepts images and video, has a context window of 1,000,000 tokens and runs with a daily quota. NVIDIA's model card describes it as a vision-language model for reasoning, coding, tool use, document understanding and video understanding. Send the image as a base64 data URL; this row accepts json_object and json_schema in response_format and auto, required and a named function in tool_choice, and reasoning is again on by default and turned off with "reasoning": false or :fast.

For a paid row, call Qwen3.6 35B-A3B with the id qwen/qwen3.6-35b-a3b. It is billed at $0.07 input and $0.70 output per 1M tokens with no cache discount; a reading request with 10,000 input and 5,000 output tokens costs $0.0042. Qwen's model card describes it as a model with a vision encoder that runs in thinking mode by default. On LLMTR it accepts images, supports json_object and json_schema in response_format and turns reasoning off with "reasoning": false; use only auto for tool_choice, as required and a named function are not supported on this row. Top up your balance under Dashboard > Billing.

Reading a chart or table image with Intern-S2

Create an LLMTR key, turn the image into a base64 data URL and send it to Intern-S2 with a prompt that asks for the rows as JSON.

  1. Key. Open an account on llmtr.com, create a key under Dashboard > API Keys and keep it in the LLMTR_API_KEY environment variable.
  2. Image and prompt. Turn the image into a data URL with the data:image/png;base64, prefix and ask in the prompt for one JSON object per row.
  3. Request and check. Send the request to the https://llmtr.com/v1/chat/completions endpoint, check finish_reason and parse the answer with json.loads.

Frequently asked questions

Does Intern-S2 take an image as a URL or as base64?

Both: put an https address or a data URL with the data:image/png;base64, prefix in the image_url part. If the file is on your local disk, use a base64 data URL; the same form also works on Nemotron 3.5 Super VL, the row you can move to after 31 October 2026.

How do you get JSON from Intern-S2?

Do not send response_format; JSON mode is not supported on this row. Describe the format in the prompt, check the answer with a parser such as json.loads, and if finish_reason is length, raise max_tokens and send the request again.

Can Intern-S2 be used after 1 November 2026?

No. From 1 November 2026, 00:00 (TRT), requests return 410 model_retired and are not redirected to another model. To keep going free of charge after 31 October 2026, use nvidia/nemotron-3.5-super-vl-free; for a paid row, use qwen/qwen3.6-35b-a3b.

Related posts