---
title: "Trim API Responses for LLM Context: Fewer Tokens per Call"
description: "Cut a Google Shopping response from 1,358 to 422 characters by keeping 8 fields. Python code, a measured before and after, and 1 credit per call."
url: https://perceptlayer.com/blog/trim-api-responses-llm-context
---

[Blog](https://perceptlayer.com/blog) / [AI agents](https://perceptlayer.com/blog/category/ai-agents)

# Trim API Responses for LLM Context: Fewer Tokens per Call

19 Oct 2026 Perceptlayer 8 min read

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.

## Key takeaways

-   Trimming happens in your code. The `/v1/google-shopping/search` endpoint takes no field-selection parameter, so you filter the JSON after it arrives.
-   On the recorded `iphone 15` response, a whitelist of 8 fields shrank the compact JSON from 1,358 to 422 characters, a 68.9% reduction.
-   The biggest single fields in the recording were `product_token`, `thumbnail` and `link`. None of them helps a model compare prices.
-   Trimming doesn't change cost: a successful call is 1 credit, a failed call is 0, with or without a filter.
-   We measured characters, not tokens. Count tokens with your own model's tokenizer before you budget.

## 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](https://platform.claude.com/docs/en/build-with-claude/token-counting) (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](https://perceptlayer.com/blog/agent-tool-schema-design-web-apis): 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 `…`:

```bash
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:

```json
{
  "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`.

```python
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](https://docs.perceptlayer.com/errors) and [handling web data API errors in Python](https://perceptlayer.com/blog/handle-web-data-api-errors-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:

```json
{"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](https://github.com/openai/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](https://perceptlayer.com/blog/paginate-web-data-api-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](https://perceptlayer.com/blog/mcp-vs-function-calling-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](https://docs.perceptlayer.com/ai-agents) 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:

```python
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](https://perceptlayer.com/blog/ai-agent-web-data-costs).

## 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](https://perceptlayer.com/pricing) has what they cost. Read `X-Credits-Remaining` on any response to see your balance, and see [credits](https://docs.perceptlayer.com/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](https://perceptlayer.com/blog/cache-web-data-api-responses) 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:

```python
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](https://perceptlayer.com/signup), then run the call in the playground on the [Google Shopping API](https://perceptlayer.com/apis/google-shopping) page and paste the response into the script's `slim()` function.

## APIs in this post

-   [Google Shopping **1 credit per call** · 1 endpoint](https://perceptlayer.com/apis/google-shopping)

## 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](https://docs.perceptlayer.com/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](https://perceptlayer.com/blog/paginate-web-data-api-python).

llm token usage context window google shopping ai agents

## More from the blog

1.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 12 Oct 2026 Perceptlayer 7 min read
    
    ## [MCP Server Too Many Tools: Keeping Agent Context Small](https://perceptlayer.com/blog/mcp-too-many-tools-context)
    
    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.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 17 Sept 2026 Perceptlayer 8 min read
    
    ## [Build a Price Tracking Agent in Python with Live Data](https://perceptlayer.com/blog/price-tracking-agent-python)
    
    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.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 23 Oct 2026 Perceptlayer 8 min read
    
    ## [Build a Market Research Agent with Search and Trends Data](https://perceptlayer.com/blog/market-research-agent-search-trends)
    
    Wire Google Search, Trends and News into one Python agent, cap it at 8 credits per question, and see a recorded Trends response.
