{"slug": "how-ai-actually-calls-an-api-tool-calling-explained-from-scratch", "title": "How AI Actually Calls an API? Tool Calling Explained from Scratch", "summary": "A developer demonstrated how tool calling works in practice, showing that a foundation model does not execute code itself but instead emits a structured request that application code reads and runs. Using Amazon Bedrock's Converse API with a Claude model, the engineer built a get_weather tool backed by the Open-Meteo API, then extended the example to two tools and a fact-injection technique. \"The model is the decision-maker. Your code is the hands,\" the developer wrote, with sample code published in a GitHub repository.", "body_md": "In the [previous post](https://dev.to/aws/why-rag-gives-wrong-answers-and-how-to-fix-retrieval-failures-1234), we taught a model to read our documents. It could search a pile of files and answer from them, which was very useful.\n\nBut I still couldn't ask it if it was going to rain, check a live price or even what today's date is.\n\nBecause as we discussed this earlier, a foundation model on its own is frozen in time. Its knowledge stops at its training cutoff and it's locked in a box. No window to the outside world.\n\nThis post is about that window, tool calling. We give the model one tool and watch it reach out for live data, add a second tool, then get into the two very different ways an app can hand a model a fact it doesn't have. One of those two is the reason one of the AI assistant you've used can tell you today's date.\n\nAll the code is in [my GitHub repo](https://github.com/gaonkarr/learning-ai-out-loud-samples-for-aws), in the `ep07-tool-calling` folder. Three tiny scripts, one idea each: one tool, two tools, and the injection trick.\n\nWhen I first heard \"the model calls a tool,\" I pictured the model reaching out and running code by itself.\n\nThat is not what happens.\n\nThe model does not run anything because it really can't. It is still just reading a prompt and producing text.\n\nWhat it produces is a structured request that says \"I'd like to call this tool, with these inputs.\"\n\nIt just hands you a note, that your code reads and then runs the actual tool. It then hands the result back to the model for further actions, either to tell you the answer or call another tool.\n\nThe model is the decision-maker. Your code is the hands.\n\nThis is run every single time:\n\n`call get_weather, city is Toronto`.\nI'm using [Amazon Bedrock](https://aws.amazon.com/bedrock?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&sc_channel=el) again, same as the whole series, calling a Claude Model through the Converse API. Converse has a spot built in for tools, called `toolConfig`.\n\n```\nresponse = bedrock.converse(\n    modelId=MODEL,\n    messages=messages,\n    toolConfig={\"tools\": [WEATHER_TOOL]},\n    inferenceConfig={\"maxTokens\": 2048},\n    additionalModelRequestFields=THINKING,\n)\n```\n\nLet's take the simplest tool to start: get the weather.\n\nDescribing a tool to the model is three parts: a name, a plain-English description, and an input schema for the arguments.\n\n```\nWEATHER_TOOL = {\n    \"toolSpec\": {\n        \"name\": \"get_weather\",\n        \"description\": \"Get the current weather for a single city.\",\n        \"inputSchema\": {\n            \"json\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"city\": {\n                        \"type\": \"string\",\n                        \"description\": \"A plain city name, e.g. Toronto or Paris.\",\n                    }\n                },\n                \"required\": [\"city\"],\n            }\n        },\n    }\n}\n```\n\nThis description and schema are the only things the model reads to decide when and how to use this tool.\n\nYour tool description is a prompt, so treat it like one.\n\nAnd separately, the real function that does the work:\n\n``` python\nimport requests\n\n# Open-Meteo returns a numeric weather_code; map the ones we need to plain words.\nWEATHER_CODES = {0: \"clear sky\", 2: \"partly cloudy\", 3: \"overcast\", 61: \"light rain\", 63: \"moderate rain\"}\n\ndef get_weather(city: str) -> dict:\n    geo = requests.get(\n        \"https://geocoding-api.open-meteo.com/v1/search\",\n        params={\"name\": city, \"count\": 1},\n    ).json()[\"results\"][0]\n    now = requests.get(\n        \"https://api.open-meteo.com/v1/forecast\",\n        params={\n            \"latitude\": geo[\"latitude\"],\n            \"longitude\": geo[\"longitude\"],\n            \"current\": \"temperature_2m,weather_code,wind_speed_10m\",\n        },\n    ).json()[\"current\"]\n    return {\n        \"city\": geo[\"name\"],\n        \"country\": geo[\"country\"],\n        \"temperature_c\": now[\"temperature_2m\"],\n        \"conditions\": WEATHER_CODES.get(now[\"weather_code\"], \"unknown\"),\n        \"wind_kph\": now[\"wind_speed_10m\"],\n    }\n```\n\nThis is normal code. No AI in it. It hits [Open-Meteo](https://open-meteo.com/), a free weather API with no key required.\n\n**Question:** \"Do I need an umbrella in Toronto today?\"\n\nI send that to the model along with the `get_weather` definition. \n\nThe model stops with a `stopReason` of `tool_use` and hands back a request:\n\n```\n{\n  \"toolUse\": {\n    \"toolUseId\": \"tooluse_abc123\",\n    \"name\": \"get_weather\",\n    \"input\": { \"city\": \"Toronto\" }\n  }\n}\n```\n\nI never told it which tool to use, and I never told it the argument. It read one question and worked out both. But nothing has run yet.\n\nSo my code runs `get_weather(\"Toronto\")`, hits the API, and gets back the real conditions. Then I package that up and send it back to the model as a `toolResult`:\n\n```\nmessages.append({\n    \"role\": \"user\",\n    \"content\": [{\n        \"toolResult\": {\n            \"toolUseId\": \"tooluse_abc123\",\n            \"content\": [{\"json\": {\n                \"city\": \"Toronto\",\n                \"country\": \"Canada\",\n                \"temperature_c\": 23.8,\n                \"conditions\": \"overcast\",\n                \"wind_kph\": 3.9,\n            }}],\n        }\n    }],\n})\n```\n\nWith a single tool, the whole thing is a straight line. Send, get the request, run it, send the result back, get the answer. Top to bottom, no loop:\n\n```\nmessages = [{\"role\": \"user\", \"content\": [{\"text\": QUESTION}]}]\n\n# 1. Send the question + the tool.\nresponse = bedrock.converse(\n    modelId=MODEL,\n    messages=messages,\n    toolConfig={\"tools\": [WEATHER_TOOL]},\n)\nmessages.append(response[\"output\"][\"message\"])\n\n# 2. The model asks for the tool. 3. Run it. 4. Send the result back.\ntool_request = next(\n    b[\"toolUse\"] for b in response[\"output\"][\"message\"][\"content\"] if \"toolUse\" in b\n)\nresult = get_weather(tool_request[\"input\"][\"city\"])\nmessages.append({\n    \"role\": \"user\",\n    \"content\": [{\n        \"toolResult\": {\n            \"toolUseId\": tool_request[\"toolUseId\"],\n            \"content\": [{\"json\": result}],\n        }\n    }],\n})\n\n# The model writes the final answer, grounded in the real data.\nfinal = bedrock.converse(modelId=MODEL, messages=messages, toolConfig={\"tools\": [WEATHER_TOOL]})\n```\n\nOne tool, one round trip. I know exactly what's going to happen, so I can just write it out.\n\nWith real data in hand, the model writes the answer: *\"Based on the current weather in Toronto, **you probably don't need an umbrella right now**.\"*\n\nThat answer did not exist anywhere in the model. It went from frozen to current in one tool call.\n\nNow something that feels like it should be trivial.\n\n**Question:** \"What's today's date?\"\n\nNo tool call comes back. The model just says, plainly, that it doesn't have access to the current date.\n\nThe only tool it has access to is weather, so nothing here can reach a date. It can't answer, and this is the part I love, it doesn't pretend to. It just tells me it doesn't know, which is a real shift from [the hallucinations post](https://dev.to/aws/why-does-ai-sometimes-lie-hallucinations-explained-abcd).\n\nIf the problem is \"there's no tool for the date,\" the fix is obvious, lets give it one.\n\n```\nDATETIME_TOOL = {\n    \"toolSpec\": {\n        \"name\": \"get_current_datetime\",\n        \"description\": \"Get the current date and time.\",\n        \"inputSchema\": {\"json\": {\"type\": \"object\", \"properties\": {}}},\n    }\n}\n\ndef get_current_datetime() -> dict:\n    from datetime import datetime\n    now = datetime.now()\n    return {\n        \"date\": now.strftime(\"%Y-%m-%d\"),\n        \"day_of_week\": now.strftime(\"%A\"),\n        \"time\": now.strftime(\"%H:%M\"),\n    }\n```\n\nNo arguments, no AI, it just returns today's date and time. I add it to the list of tools the model is allowed to use. Now the model has two tools - weather and date.\n\n**Question:** \"Do I need an umbrella in Toronto? And what is today's date?\"\n\nTwo requests come back, for two tools. `get_weather` with `{\"city\": \"Toronto\"}`, then `get_current_datetime` with `{}`. My code runs each one, hands both results back, and the model writes one answer using both.\n\nOne sentence, two different needs, right tool for each. It just routed it.\n\nBut notice the problem with my nice straight line from before. With one tool, I knew there'd be exactly one round trip. With two, I don't know which the model will pick, or how many, or whether it'll come back for more after seeing the first result. So the four steps go inside a loop. Keep going while the model keeps asking for tools, and stop when it writes the answer instead:\n\n```\n# name → the real function to run when the model asks for it.\nTOOLS = {\n    \"get_weather\": get_weather,\n    \"get_current_datetime\": get_current_datetime,\n}\n\nmessages = [{\"role\": \"user\", \"content\": [{\"text\": QUESTION}]}]\n\nwhile True:\n    response = bedrock.converse(\n        modelId=MODEL,\n        messages=messages,\n        toolConfig={\"tools\": [WEATHER_TOOL, DATETIME_TOOL]},\n    )\n    assistant_message = response[\"output\"][\"message\"]\n    messages.append(assistant_message)\n\n    # Done? The model stopped asking for tools and wrote its answer.\n    if response[\"stopReason\"] != \"tool_use\":\n        answer = \"\".join(b[\"text\"] for b in assistant_message[\"content\"] if \"text\" in b)\n        break\n\n    # Otherwise: run every tool the model requested, send the results back.\n    tool_results = []\n    for block in assistant_message[\"content\"]:\n        if \"toolUse\" not in block:\n            continue\n        request = block[\"toolUse\"]\n        result = TOOLS[request[\"name\"]](**request[\"input\"])\n        tool_results.append({\n            \"toolResult\": {\n                \"toolUseId\": request[\"toolUseId\"],\n                \"content\": [{\"json\": result}],\n            }\n        })\n    messages.append({\"role\": \"user\", \"content\": tool_results})\n```\n\nThat `while` loop is the whole difference. One tool was a straight line I could hardcode. More than one, and I hand the control to the model and let it drive until it's done. \n\nThis is so so so important to understand, because this is a seed of an agent!\n\nThis is the part that bugged me while I was learning. If a raw model doesn't know today's date, how does ChatGPT or Claude or any AI assistant know it? You ask what day it is and they answer instantly. Are they calling a date tool every time? Short answer, no.\n\nAnthropic actually publishes the system prompt they use for Claude, in their [release notes](https://docs.anthropic.com/en/release-notes/system-prompts). They say Claude's web interface and mobile apps use a *system prompt* to provide up-to-date information, such as the current date, at the *start of every conversation*.\n\nThat's it. No tool runs. It's just text thats slipped into the instructions before your message ever gets there. The model was handed the date as context.\n\nYou can do the exact same thing in a script. Take away the date tool and paste today's date into the system prompt as plain text:\n\n```\nsystem_prompt = [{\n    \"text\": f\"Today's date is {datetime.now():%A, %d %B %Y}.\"\n}]\n```\n\nAsk \"what's today's date?\" and it answers, correctly, with no tool call at all. Because you handed it the date.\n\nSo there are two ways to give a model a fact it doesn't have. A tool it calls and you run, or context you inject straight into the prompt. When do you use which?\n\nAnd remember that schema, `city` and nothing else? That's why I can't ask this thing about next week. There's no date to pass in. If I wanted a forecast, that's a different tool.\n\nSo we've got two tools working. Weather and date. Great. But real systems don't have just two tools. They have dozens - check the calendar, search the CRM, query the database, send the email and/or read the file.\n\nAnd with what we just built, every one of those is something I hand-wire myself - write the schema, write the function, register it, keep the description in sync when the tool changes.\n\nFor two tools, that's fine. Fifty tools, across five apps, all changing over time? That's a maintenance nightmare. And everyone building AI apps was writing the same glue code, over and over, for the same tools.\n\nThis is the problem MCP solves. MCP stands for **Model Context Protocol**. It's an open standard, started by Anthropic and now used across the industry, for how AI apps and tools talk to each other.\n\nThe clean way to think about it: MCP is like USB-C for AI tools. Before USB-C, every device had its own cable and connector. It was a chaos of cables. USB-C is one standard plug. MCP is that, but for connecting models to tools and data.\n\nThe tool lives behind an MCP server, and that server describes itself: here are the tools I offer, here's what each does, here are the inputs I need. Your app is the MCP client. It just asks \"what have you got?\" and the server tells it. The tools get discovered at runtime.\n\nSo if someone builds an MCP server for GitHub, or your database, or Slack, you don't write the integration. You point your app at the server and the tools show up.\n\nWe're not building one today, that's a whole topic on its own. The mental model is enough for now: tool calling is how one model uses a tool, and MCP is how any model discovers and uses tools.\n\n**If you're just getting started:** Tool calling is how AI stops being a closed box. Give it tools and it can pull live information and take action instead of just talking. The one thing to hold onto: the model is the brain, your code is the hands.\n\n**If you're more on the builder side:** The model picks the tool and fills in the arguments, and the only thing it reads to make that call is your description and schema. So write them like prompts, and be specific about what the tool does and doesn't do. Then: static facts get injected, live facts get a tool. And once you're past a couple of tools, stop hardcoding and look at MCP.\n\nToday the model called one tool, or two, once each, then answered. But what happens when a question needs several tools, in the right order? Check my calendar, then check the weather for that day, then draft the email. The model has to plan, act, look at the result, and decide the next step. Over and over in a loop, until it's done.\n\nWell, that loop is actually called an agent. And next post, we build one with [Strands Agents SDK](https://strandsagents.com/?trk=44b16281-e090-49b6-97d8-f1cea54d9e87&sc_channel=el).\n\nRide along.\n\n*This post is part of the \"Learning AI Out Loud\" series, a cloud architect learning AI from first principles.*", "url": "https://wpnews.pro/news/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch", "canonical_source": "https://dev.to/aws/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch-4lf8", "published_at": "2026-09-16 21:28:40+00:00", "updated_at": "2026-09-16 21:53:10.703551+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "large-language-models", "ai-products", "developer-tools"], "entities": ["Amazon Bedrock", "Claude", "AWS", "Open-Meteo", "GitHub", "Converse API"], "alternates": {"html": "https://wpnews.pro/news/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch", "markdown": "https://wpnews.pro/news/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch.md", "text": "https://wpnews.pro/news/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch.txt", "jsonld": "https://wpnews.pro/news/how-ai-actually-calls-an-api-tool-calling-explained-from-scratch.jsonld"}}