Function calling
Giving an LLM 309 endpoints as 309 tools is the obvious approach and the wrong one. Here is the shape that works, and the four failures worth designing against.
Do not define 309 tools
Every major provider degrades as the tool list grows — the schemas consume the context window before the conversation starts, and selection accuracy falls once several tools have overlapping descriptions. With 309 endpoints, most of which take the same four fields, you would be spending thousands of tokens per turn to make the model worse at choosing.
Define one tool, or two. The model picks the route as an argument rather than by selecting among hundreds of schemas.
The one-tool shape
{
"name": "occult_api",
"description":
"Vedic astrology calculations from the Occult API. Call this for anything involving a birth chart, panchanga, dasha, muhurta, matching or numerology. Do not compute astrology yourself - planetary positions require an ephemeris and cannot be derived from a date.",
"input_schema": {
"type": "object",
"properties": {
"route": {
"type": "string",
"description": "Endpoint path, e.g. /api/astro/panchanga/. Use the catalogue at https://occultapi.com/llms.txt to choose one."
},
"body": {
"type": "object",
"description": "Request body. Always include date_time (ISO 8601 with offset), latitude, longitude, timezone_as_float. Batch calculations with a keys array where the endpoint supports it."
}
},
"required": ["route", "body"]
}
}The description does two jobs. It says when to reach for the tool, and it says not to do the arithmetic itself — which matters more than it sounds. A model asked for a nakshatra will happily produce a plausible one from training data if nothing tells it that planetary positions require an ephemeris.
The handler
async function occultApi({ route, body }) {
const response = await fetch(`${process.env.OCCULT_API_BASE}${route}`, {
method: "POST",
headers: {
"X-API-Key": process.env.OCCULT_API_KEY, // server-side only
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const payload = await response.json();
// 402 means out of credits. It is NOT transient - retrying spends nothing
// and fixes nothing, so surface it rather than backing off.
if (response.status === 402) {
return { error: "insufficient_credits", retryable: false };
}
// The envelope is the same on every endpoint: branch on is_error, and hand
// the model the message rather than a stack trace it cannot act on.
if (payload.is_error) {
return { error: payload.message, retryable: false };
}
return payload.data;
}Three things in there are worth keeping when you adapt it. The key is read server-side — a key in browser code is a key someone else is now spending. 402 is not retried — it means the balance is empty, so a backoff loop burns wall-clock and fixes nothing. The error message is passed through — the API says which field was wrong, and a model given that will fix its own request on the next turn.
The two-tool shape, when the model needs to explore
If your users ask open-ended questions, the model will not know which route it needs. Give it a search tool as well — searching costs no credits, so the exploration is free and only the answer is billed.
[
{
"name": "occult_search",
"description": "Find the right Occult API endpoint by name or by what it calculates, e.g. 'manglik' or 'sunrise'. Returns routes and their request fields. Costs nothing.",
"input_schema": {
"type": "object",
"properties": { "query": { "type": "string" } },
"required": ["query"]
}
},
{
"name": "occult_call",
"description": "Call an Occult API endpoint found with occult_search. One credit per successful call; failures are free.",
"input_schema": {
"type": "object",
"properties": {
"route": { "type": "string" },
"body": { "type": "object" }
},
"required": ["route", "body"]
}
}
]Point occult_search at /llms.txt or the fuller /llms-full.txt, both of which exist for exactly this. There is also an OpenAPI 3.1 spec if your framework generates tools from one.
Four failures worth designing against
- One call per calculation. Most endpoints take a
keysarray and will compute several things in one request for one credit. A model left to itself will call four times and spend four. Say so in the tool description. - Retrying a 402. It is a balance problem, not a network problem. Mark it non-retryable in the handler, because the model cannot tell the difference from the status code alone.
- The key reaching the client. Tool execution belongs on your server. The model never needs to see the key, and any design where it could is one prompt injection away from leaking it.
- Invented field names. Models guess
dateorlat. The real fields aredate_time,latitude,longitudeandtimezone_as_float. Put them in the description rather than letting the model discover them through a 400.
What a call actually looks like
curl -X POST https://api.occultapi.com/api/astro/panchanga/ \
-H 'X-API-Key: yt_live_your_key_here' \
-H 'Content-Type: application/json' \
-d '{
"date_time": "2026-09-17T06:00:00+05:30",
"latitude": 26.9124,
"longitude": 75.7873,
"timezone_as_float": 5.5,
"keys": ["tithi", "nakshatra", "yogam", "karana"]
}'Four calculations, one credit. The response carries X-Credits-Charged and X-Credits-Remaining, which is the cheapest way to show a running cost in your own UI.