Blog / AI agents

Trim API Responses for LLM Context: Fewer Tokens per Call

To reduce API response tokens for an LLM, don't hand the model the raw JSON. Call the API in your own code, keep only the fields the model needs, and pass that slimmer object back as the tool result. On a recorded Google Shopping response, keeping 8 fields cut the payload from 1,358 to 422 characters, and the call still costs 1 credit.

Why trim an API response before the model sees it?

Everything a tool returns goes into the model's context window, and every character of it is read on every later turn of the conversation. Fields the model never uses still cost input tokens, still dilute the useful data, and still count against the context limit. Trimming is the cheapest fix: no new infrastructure, one function.

Tokens are the unit models bill and limit by, and they don't map one to one onto characters. Tokenizers differ between model generations: Anthropic's token counting docs (2026) say newer Claude models produce roughly 30 percent more tokens for the same text than earlier ones, and advise recounting against the model you'll use. That's why this article reports characters, which we can measure exactly, and leaves token counts to you.

Characters are still a fair proxy for direction. Removing 936 of 1,358 characters of JSON removes most of the text the tokenizer would have processed. Opaque IDs and encoded blobs are the first candidates to drop.

If you're wiring this into an agent loop, the same logic applies to tool schema design: the model should see only what helps it choose and use a tool.

What does the Google Shopping response contain?

The search endpoint returns three top-level objects: search_parameters (an echo of your request), shopping_results (the products) and pagination. Each product carries identifiers, display fields, a price in two forms and a few fields that exist for the site's own use. Here is the recorded response for the request below, cut to the first product and trimmed with …:

curl "https://api.perceptlayer.com/v1/google-shopping/search?q=iphone+15&gl=us&hl=en" \
  -H "X-API-Key: $API_KEY"

Only q is required. The full parameter list for /v1/google-shopping/search:

Parameter Type Required Limit or allowed values
q string Yes Up to 500 characters
gl string No Up to 2 characters
hl string No Up to 2 characters
location string No Up to 63 characters
uule string No Up to 500 characters
start integer No n/a
device string No desktop, mobile or tablet
small_business boolean No n/a
shoprs string No Up to 2000 characters

None of them selects fields. The response looks like this:

{
  "search_parameters": { "q": "iphone 15", "gl": "us", "hl": "en", "start": 0, "device": "desktop" },
  "shopping_results": [
    {
      "position": 1,
      "category": "Popular products",
      "title": "Apple iPhone 15",
      "source": "Walmart - Wireless Source",
      "rating": 4.7,
      "reviews": 45000,
      "extensions": [" & more"],
      "product_token": "eyJlaSI6InZaeUE0IiwicHJvZHVjdGlkIjoiNDI2MzYwNjAxNzQxMTU2NTk5MiIs…",
      "rank_absolute": 1,
      "product_id": "4263606017411565992",
      "link": "https://www.google.com/shopping/product/4263606017411565992",
      "price": "$393.73",
      "extracted_price": 393.73,
      "thumbnail": "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
    },
    …
  ],
  "pagination": { "current_page": 1, "start": 0, "next_start": 10 }
}

The recording has two products. Counting every character of each product object in the compact JSON, the fields break down like this for the pair together:

Field Characters (2 products) Useful to a price-comparison model?
product_token 326 No. Opaque string, truncated with … in the recording
thumbnail 168 No. A base64 data URI, not an image URL, in this recording
link 122 Sometimes, if the agent must cite or open the product
product_id 42 Only if you fetch the product later
title 40 Yes
source 37 Yes. It's the seller
category 36 Rarely
extensions 22 Rarely
price 18 Redundant with extracted_price
extracted_price 9 Yes. A number, so the model can compare
reviews 7 Yes
rating 6 Yes
position, rank_absolute 2 each Optional

Counts include the quoted value and nothing else: they exclude key names and punctuation. So they understate the real saving, because each dropped field also removes its key.

Which fields should you keep?

Keep the fields the model needs to answer the question it was asked, and drop everything else. For a "who sells the iPhone 15 cheapest, and is it well reviewed?" question, that means title, source, extracted_price, rating and reviews. Add link if the answer should point somewhere, and position if rank matters.

Two judgment calls come up with this response:

  • price versus extracted_price. price is the display string ("$393.73"), and extracted_price is the same figure as a number. Keep the number for comparisons. Keep the string only if you want the currency symbol in the model's answer, since a bare number has none.
  • product_id and link. If a later step looks up a single product, the identifier is useful. If the agent only reports results, link alone covers citation.

Don't drop fields you can't reconstruct. If you might need a field in a follow-up step, store the full response in your application (not in the model's context) and give the model a reference to it. That also keeps the trimmed text honest, since the original is still there to audit.

How do you filter the response in Python?

Write a small whitelist function and call it between the HTTP response and the tool result. The function below keeps eight fields per product, drops everything else, and prints the length of each version so you can measure the saving on your own queries. It only needs the requests package and your key in API_KEY.

import json
import os

import requests

URL = "https://api.perceptlayer.com/v1/google-shopping/search"
KEEP = ("position", "title", "source", "price", "extracted_price",
        "rating", "reviews", "link")


def search(q, gl="us", hl="en"):
    r = requests.get(
        URL,
        params={"q": q, "gl": gl, "hl": hl},
        headers={"X-API-Key": os.environ["API_KEY"]},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()


def slim(data, keep=KEEP):
    return [
        {k: item[k] for k in keep if k in item}
        for item in data.get("shopping_results", [])
    ]


def size(obj):
    return len(json.dumps(obj, separators=(",", ":"), ensure_ascii=False))


if __name__ == "__main__":
    full = search("iphone 15")
    trimmed = slim(full)
    print("full:", size(full), "trimmed:", size(trimmed))
    print(json.dumps(trimmed, indent=2))

Two details matter. k in item skips fields a product doesn't have instead of raising, so a result with no rating doesn't break the loop. And separators=(",", ":") measures compact JSON, which is what you should send the model: pretty-printing adds whitespace you pay for.

Errors still need handling. raise_for_status() turns a 4xx or 5xx into an exception, and the body carries a code you can branch on. See the error reference and handling web data API errors in Python for retry logic.

What is the measured before and after?

On the recorded response, the full compact JSON is 1,358 characters. The same two products after the 8-field whitelist, as a bare list, come to 422 characters: 936 fewer, a 68.9% reduction. Cutting further to five fields (title, source, extracted_price, rating, reviews) gives 222 characters, an 83.7% reduction.

Version Fields kept per product Characters (compact JSON) Change vs full
Full recorded response all 14, plus search_parameters and pagination 1,358 n/a
8-field whitelist position, title, source, price, extracted_price, rating, reviews, link 422 -68.9%
5-field whitelist title, source, extracted_price, rating, reviews 222 -83.7%

The 8-field output for the first product looks like this:

{"position":1,"title":"Apple iPhone 15","source":"Walmart - Wireless Source","price":"$393.73","extracted_price":393.73,"rating":4.7,"reviews":45000,"link":"https://www.google.com/shopping/product/4263606017411565992"}

Read these numbers with their limits in mind:

  • Two products only. The recording holds two results, so the percentages describe this sample. Another query or a longer result list can shift them.
  • Truncated token. The recorded product_token ends in …, so the original strings are longer than what we counted. The real saving on that field is at least what's shown.
  • Characters, not tokens. We didn't run a tokenizer. A drop in characters should lower your token count, but by how much depends on the model. Measure with your provider's counter, for example tiktoken for OpenAI models.
  • Not a like-for-like shape. The 422 figure is a bare list of products, measured against the full object with search_parameters and pagination, so part of the saving comes from dropping those two. Keep pagination.next_start if the agent pages, as covered in paginating a web data API in Python.

How does this fit an agent or MCP tool call?

Put the filter inside the tool your agent calls, so the model only ever sees the slim version. With a function-calling setup, that's the body of your tool function: call the endpoint, run slim(), return the result. MCP vs function calling for live web data covers where each fits.

With the Perceptlayer MCP server at https://api.perceptlayer.com/mcp, call_endpoint takes an endpoint ID and that endpoint's parameters and returns the endpoint's JSON response. The MCP docs cover setup. Its output reaches the model as returned, so for trimmed output call the HTTP API from your own tool and expose only that wrapper to the model. Reusing search() and slim() from above:

def shopping_tool(q: str) -> str:
    """Tool body: search, trim, return compact JSON text."""
    try:
        data = search(q)
    except requests.HTTPError as e:
        # Failed calls cost 0 credits; tell the model briefly, not the whole body.
        return json.dumps({"error": e.response.status_code})
    # The tool result is the trimmed list, not the raw response.
    return json.dumps(slim(data), separators=(",", ":"))

Register shopping_tool with your framework as the tool function. The model-facing contract stays small: describe the wrapper's output in its schema (title, source, extracted_price, rating, reviews) so the model knows what it will get.

Trimming also helps when results accumulate. An agent that runs ten searches in one session carries ten tool results forward, and a leaner result is the difference between fitting them and having to summarise or evict. For the budget side, see capping web data spend for AI agents.

What does it cost?

A successful call to /v1/google-shopping/search costs 1 credit, and a failed call costs nothing and returns X-Credits-Used: 0. Filtering the response in your code changes none of that: you pay for the call, not for the bytes you keep.

Scenario Calls Credits
500 product searches 500 × 1 credit 500
20,000 product searches 20,000 × 1 credit 20,000
500 searches, 40 return errors 460 × 1 credit 460

The free tier gives you 1,000 credits every month after you verify an email address, with no credit card, so the 500-search case fits inside it. Paid credits come in prepaid packs starting at 10,000, and pricing has what they cost. Read X-Credits-Remaining on any response to see your balance, and see credits for how debits work.

Because the cost is per call, trimming saves context tokens, not credits. Reducing calls is a different lever: caching API responses in Python covers it.

How do you catch a filter that drops a field you need?

slim() skips missing keys silently, so a renamed field would vanish without an error. Pin the two fields the price question depends on with a test against a saved response:

def test_slim_keeps_price_and_seller():
    items = slim(saved_response)
    assert items and all("extracted_price" in i and "source" in i for i in items)

If an answer degrades later, add back the one field it was missing. Keep the whitelist in the KEEP tuple so that is a one-line edit.

To try it on your own query, get a free API key, then run the call in the playground on the Google Shopping API page and paste the response into the script's slim() function.

APIs in this post

Frequently asked questions

Does the Google Shopping endpoint let me choose which fields come back?

No. None of the documented parameters in the table above selects fields. Filter the JSON in your own code after the response arrives, as the Python example in this article does, and keep the full response in your application if a later step might need it.

Does trimming the response reduce what I pay in credits?

No. A successful call costs 1 credit whatever you do with the response afterwards, and failed calls cost nothing. Trimming lowers the tokens you send to the model, not the credits you spend on the API. To spend fewer credits, make fewer calls, for example by caching. See credits.

How many tokens did the trimmed response save?

We don't know, because we measured characters, not tokens. The recorded response went from 1,358 to 422 characters with an 8-field whitelist. Run your model's tokenizer on both versions of your own responses to get a token count, since tokenizers differ between model families.

Should I send the model the whole response when I'm unsure what it needs?

For a first prototype, yes: it removes one variable. Once you see which fields the model's answers use, switch to a whitelist. If you're unsure about one field, keep it while you test, then drop it after you've compared answers with and without it.

Can I ask for a smaller page of results instead of trimming fields?

The documented parameters include start, an integer offset, but no page size, and we don't document a default or a maximum. You can step through results with start, but fewer fields per product is the reliable way to shrink each page. See paginating a web data API.

  1. MCP Server Too Many Tools: Keeping Agent Context Small

    One tool per endpoint for 1,659 endpoints is about 567,000 characters of schema. A 10-step checklist to keep an MCP server's tool list small.

  2. Build a Price Tracking Agent in Python with Live Data

    A Python price tracker that checks Google Shopping and eBay once per product per run, stores history in SQLite, flags drops and costs 2 credits a product.

  3. Build a Market Research Agent with Search and Trends Data

    Wire Google Search, Trends and News into one Python agent, cap it at 8 credits per question, and see a recorded Trends response.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.