BLOG

Tool-use fine-tuning:
teaching a model your internal APIs.

Tool-use fine-tuning is training a model on examples of calling a specific set of tools correctly, so that the conventions of those tools end up in the weights instead of in the prompt. The training examples are ordinary conversations that happen to contain calls: a request, the structured call the model should emit, the result the tool returned, and the reply that follows from it. Nothing exotic is going on underneath. It is the same supervised fine-tuning machinery used for everything else, pointed at the tokens that make up a call.

The reason it matters more than it sounds is narrow and specific. A frontier model has read an enormous amount of public API surface. It has never seen yours. And the gap between those two facts is exactly the gap between a model that is impressive in a demo and one that is useful against the systems your firm actually runs on.

What a tool call really is

Strip away the SDK and there is no special mechanism. The tools available to the model are serialized into its context as JSON Schema definitions, usually in a dedicated section of the chat template. The model generates text. Some of that text, delimited by special tokens or a fixed structure the template defines, is meant to be read as a call rather than as prose. A parser in the serving layer pulls it out, your code dispatches it, and the result comes back as another message in the conversation, which the model then reads and continues from.

Two consequences follow, and both drive everything else in this post. First, "function calling" is a formatting contract, so a model can be perfectly capable of the reasoning and still fail because it emitted a shape your parser does not recognize. Second, choosing the tool and filling its arguments is generation, which means it is learnable the same way any other generation is.

The tool-calling loop, and the four points where a general model fails on private APIs Tool schemas placed in the model's context lead to the model generating a structured call, which a parser in the serving layer extracts and dispatches to an internal API. The API returns a result, which re-enters the model's context as a new message. Four failure points are marked: choosing the wrong tool among near-duplicates, inventing arguments the schema does not have, calling in an order the system rejects, and missing house conventions the schema never states. TOOL SCHEMAS IN THE CONTEXT MODEL GENERATED TEXT SHAPED AS A CALL PARSER IN THE SERVER YOUR INTERNAL API RESULT BACK INTO THE CONTEXT AS A MESSAGE WHERE A GENERAL MODEL FAILS ON A PRIVATE CATALOGUE 01 PICKS ONE OF FORTY NEAR-IDENTICAL ENDPOINTS 02 INVENTS AN ARGUMENT THE SCHEMA NEVER HAD 03 CALLS IN AN ORDER YOU REJECT 04 MISSES A CONVENTION NOBODY WROTE
The mechanism is generation plus a parser. Every failure below is a generation problem, which is why training can move it.

Why your APIs are the hard case

Public APIs are in the pretraining data. A capable open-weight model has effectively read the Stripe reference, the GitHub REST docs, a decade of Stack Overflow answers about both, and enough SQL to be dangerous. Ask it to charge a card and it has priors. Ask it to call positions.get_net_exposure against your book taxonomy and it has nothing but the schema you handed it thirty thousand tokens ago.

That produces a distinctive failure profile, and it is worth being precise about it because each item responds to a different fix:

  • Selection error. Internal catalogues grow by accretion, so they are full of endpoints that differ by one word. get_positions, get_positions_v2, get_positions_intraday, and get_book_positions all exist, three are deprecated in practice but not in the registry, and the schema descriptions do not say which.
  • Argument fabrication. The model produces a plausible field name from the public APIs it does know (start_date) where yours wants as_of, or invents an enum value that reads correctly and does not exist.
  • Semantic error inside a valid call. This is the expensive one, because the call passes schema validation and returns a number. Your firm's "close" means the 21:00 UTC mark and not the exchange close, your book_id is not the same granularity as a desk, and none of that is in the JSON Schema.
  • Sequencing and protocol error. The system expects a session token first, or requires the reconciliation job to be checked before positions are read as final. The model has no way to know an ordering that lives in a runbook.
  • Over-calling. The model calls a tool for something already answered in the conversation, which costs latency and, on a write endpoint, occasionally costs more than that.

Notice that only the second of those is a formatting problem. The rest are knowledge about your organization, encoded nowhere a general model could have found it.

Try the cheap fixes first

Fine-tuning is weeks. Improving what you put in the prompt is afternoons, and it frequently closes most of the gap. Rewrite the schema descriptions to say when to use each endpoint rather than what it returns. Delete deprecated tools from the catalogue instead of documenting them. Collapse near-duplicates behind one wrapper. Add two or three worked examples per high-traffic tool. Then, if the catalogue is large, retrieve a relevant subset of schemas per request instead of sending all of them, which is what tool retrieval and the various registry layers over MCP servers are for.

The limit of that approach is real, though, and shows up in two places. The schema tokens are paid on every request, and a catalogue of two hundred endpoints at a few hundred tokens each is a substantial fixed cost per call. More importantly, selection accuracy degrades as the list gets longer, because you have turned every request into a multiple-choice question with two hundred options, most of which look alike. Retrieval helps until the retriever's own mistakes become the dominant error, which happens sooner than people expect when the candidates are near-duplicates.

One honest counterweight before moving on: if you serve the model yourself, a static tool catalogue sitting at the front of every prompt is an ideal prefix-cache target, so the compute cost of those tokens is largely recoverable even though the context-window cost is not. Prompted schemas are cheaper than a naive token count suggests. What caching cannot fix is the accuracy curve.

Three ways to give a model your tool catalogue, compared by what each request has to carry Three request compositions shown as bars. Prompting the whole catalogue puts a very large block of schema tokens in front of a small conversation, and forces the model to select from hundreds of lookalike endpoints. Retrieving a subset shortens the schema block but introduces retriever error. Fine-tuning shortens it further and moves the selection rules and house conventions into the weights, at the cost of retraining when the specification changes. SCHEMA TOKENS THE ACTUAL TURN 01 PROMPT THE WHOLE CATALOGUE 200 SCHEMAS, RESENT EVERY TIME TURN SELECTION IS A 200-WAY MULTIPLE CHOICE, AND HALF THE OPTIONS LOOK ALIKE 02 RETRIEVE A SUBSET PER REQUEST TOP 12 SCHEMAS TURN CHEAPER, BUT THE RETRIEVER'S MISTAKES ARE NOW YOUR CEILING 03 FINE-TUNE ON THE CATALOGUE SHORT LIST TURN SELECTION + CONVENTIONS IN THE WEIGHTS NOTHING TO RESEND, BUT THE WEIGHTS NOW HAVE A VERSION, AND THE SPEC MOVES
The three options are not exclusive. Most working systems retrieve a subset and fine-tune on it.

The three approaches, side by side

Worth being blunt about the trade, because the third column is not strictly better than the first two.

Prompted schemas Tool retrieval Tool-use fine-tuning
What it changes The context Which context you build The weights
Time to first result An afternoon Days Weeks, mostly on data
Cost per request Full catalogue, every call A subset, plus a retrieval hop A short list
Scales to 200+ tools Poorly, selection degrades Yes, up to retriever error Yes, that is the point
Learns undocumented conventions Only what you can write down Same, it just selects Yes, from real examples
Adding a new tool Edit a schema file Reindex Regenerate data, retrain
Breaks when The catalogue grows Candidates look alike The API changes underneath it

What one training example looks like

A tool-use training record is a conversation, stored in whatever messages format your trainer reads, carrying the tool definitions alongside it. The definitions belong in the record rather than in a global config, because the model has to learn the mapping from a schema it can see to a call it should emit, and freezing that relationship into training config is how you end up with a model that only works when the catalogue is exactly the one it trained on.

ONE RECORD, WITH THE MASK MARKED

{
  "tools": [ /* the same JSON Schemas production will send */ ],
  "messages": [
    {"role": "system",  "content": "..."},                    // masked
    {"role": "user",    "content": "Net EURUSD exposure on the      // masked
                                    spot book at yesterday's close?"},

    {"role": "assistant", "tool_calls": [{                    // TRAINED ON
      "id": "c1", "type": "function",
      "function": {
        "name": "positions.get_net_exposure",
        "arguments": "{\"book_id\":\"FX-SPOT-1\",
                       \"symbol\":\"EURUSD\",
                       \"as_of\":\"2026-09-01T21:00:00Z\",
                       \"basis\":\"MARK_CLOSE\"}"}}]},

    {"role": "tool", "tool_call_id": "c1",                    // masked
     "content": "{\"net_usd\": -4182300.0, \"ccy\": \"USD\"}"},

    {"role": "assistant", "content": "The spot book was short       // TRAINED ON
                                      4.18m USD equivalent..."}
  ]
}

The masking is the one piece that is easy to get wrong and expensive to get wrong. Loss goes on the assistant turns only. The system prompt, the user's request, and above all the tool results are context the model must read and must never be trained to produce. Put loss on the tool result tokens and you are explicitly teaching the model to write convincing API responses, which is the precise habit that turns a helpful assistant into one that reports a position it never queried. Most trainers do the right thing by default for the user and system roles and get the tool role wrong, so check it rather than assume it.

Two things belong in the dataset that teams routinely leave out. One is examples where the correct action is not to call anything, because a model trained exclusively on successful calls learns that a tool call is always the answer. The other is recovery: a call that returned an error or an empty result, followed by the corrected call. Without those, the first production error puts the model somewhere its training never went.

Where the data comes from

Almost nobody has a labelled corpus of natural-language requests paired with correct calls. You build one, and there are three practical sources. The unusual luck of this problem is that you get a grader nearly for free: a call either validates against the schema and succeeds against a staging replica, or it does not, so you can filter machine-generated data at scale without human review.

LOGS YOU ALREADY HAVE

Gateway logs, audit trails, and client SDK traces record calls that definitely worked, with real argument distributions. What they lack is the request that motivated each one, which has to be written back on, by hand or with a model.

SYNTHESIS FROM THE SPEC

Sample valid calls directly from the OpenAPI or JSON Schema definitions, execute them against a staging replica, and keep the ones that return. Cheap, unlimited, and covers endpoints nobody has used yet.

REJECTION SAMPLING

Let the current model attempt a set of real requests, keep only the attempts a validator accepts, and train on those. This is the loop that keeps paying after the first round, because each version generates better data than the last.

The synthesis path deserves a diagram, because the ordering is counterintuitive. You do not start from questions and try to produce calls. You start from calls, which you can generate correctly by construction, and work backwards to the question that would have prompted each one.

Generating tool-use training data backwards from the API specification The OpenAPI specification is sampled to produce valid calls, which are executed against a staging replica. Calls that return successfully are back-translated into the natural-language request a user would have asked. A round-trip check then asks the model to regenerate the call from that request alone; matching pairs enter the dataset and mismatches are sent back for review, which usually reveals an ambiguous request rather than a model failure. Production gateway logs feed into the same back-translation step as a second source. OPENAPI / JSON SCHEMA SAMPLE VALID CALLS RUN AGAINST A STAGING REPLICA KEEP WHAT RETURNS 200 PRODUCTION GATEWAY LOGS BACK-TRANSLATE CALL → REQUEST ROUND-TRIP CHECK REQUEST → CALL? MATCH: KEEP MISMATCH: THE REQUEST WAS AMBIGUOUS. FIX IT, NOT THE MODEL. GENERATE FROM THE SPEC, SO EVERY CALL IS VALID BY CONSTRUCTION. THE HARD PART IS THE QUESTION, NOT THE CALL.
Backwards generation. The round-trip check is also your first evaluation set, so build it before you need it.

The chat template trap

This is the bug that eats the first week, and it is entirely avoidable. The syntax of a tool call is not defined by the model, it is defined by the model's chat template: the Jinja template shipped alongside the tokenizer that turns a list of messages into a single token sequence. Different model families use genuinely different conventions, some wrapping calls in special tokens, others in tagged blocks, others in bare JSON.

Your inference server has a matching parser on the other side. Serving stacks expose it as an explicit choice of tool-call parser, and that parser is looking for one specific shape. So if you fine-tune the model to emit a format of your own devising, everything will look fine in your training logs and then, in production, every call will arrive as ordinary text in the content field with no tool call detected. The rule is simple: render your training data through the exact chat template you will serve with, and do not invent a format. If you genuinely need new special tokens, they have to be added to the tokenizer and their embeddings trained, which is a much larger change than it appears.

Worth pairing with this: constrained decoding, where the serving layer restricts generation to tokens that keep the output valid against a grammar or JSON Schema, makes syntactically invalid calls impossible by construction. It is not a substitute for fine-tuning, because a call can be perfectly valid and completely wrong, but it is a good complement. Guarantee the syntax mechanically and let training spend its capacity on choosing the right tool and the right values.

How much training this actually takes

Less than people assume, because you are not teaching a new capability. The model already knows how to call tools. You are teaching it a specific vocabulary and a set of house conventions, which is a comparatively shallow change, and that has three practical consequences.

  • Parameter-efficient methods are usually enough. LoRA at a modest rank moves this behavior reliably, trains in hours rather than days, and produces an adapter you can swap or roll back without touching the base weights. That last property matters more here than in most fine-tuning work, for reasons the next section makes clear.
  • The dataset is smaller than you fear. Hundreds to a few thousand well-covered examples per tool family, with real argument diversity, beats a much larger set that repeats the same three shapes. Coverage of the awkward cases is worth more than volume.
  • Smaller models punch above their weight here. This is a narrow, well-specified job, and a mid-sized open-weight model trained on your catalogue routinely outperforms a far larger general model working from prompted schemas, at a fraction of the serving cost. It is one of the clearer cases where owning a small language model beats renting a big one.

Two cautions on the recipe. Mix a slice of general instruction data into the training mix, or you will trade broad competence for tool accuracy and notice it only when someone asks the model an ordinary question. And hold out a few tools entirely, never training on them, so you can measure whether the model learned to read schemas in general or merely memorized the ones it saw.

How to evaluate it

Validation loss is close to useless for this. It moves a little while behavior moves a lot, and it cannot distinguish a call that is wrong from a call that is right but phrased differently. Measure outcomes instead, as a ladder of increasingly strict conditions, and report every rung rather than a single headline number. The rung where the number collapses tells you what to fix.

The evaluation ladder for a tool-use fine-tune Six ascending steps of increasingly strict evaluation: the output parses as a call at all, it validates against the schema, it selects the right tool, the argument values are semantically correct, the sequence of calls is correct, and the task is actually completed. A note points out that validation loss is not on the ladder, and that over-calling, meaning the model invoking a tool when none was needed, must be tracked separately because it rises alongside the other numbers. REPORT EVERY RUNG, NOT A HEADLINE NUMBER THE RUNG WHERE THE NUMBER COLLAPSES IS THE ONE TELLING YOU WHAT TO FIX. PARSES AT ALL SCHEMA VALID RIGHT TOOL RIGHT ARGUMENTS RIGHT SEQUENCE TASK ACTUALLY DONE NOT A RUNG. TRACK IT SEPARATELY, IT OFTEN RISES WHILE EVERY RUNG DOES: OVER-CALLING: A TOOL NOBODY NEEDED VALIDATION LOSS IS NOT ON THIS LADDER. IT BARELY MOVES WHILE THE BEHAVIOR DOES.
Six rungs, reported separately. Most tool-use fine-tunes clear the bottom two immediately and live or die on the middle three.

Break every rung down per tool, too. Aggregate accuracy hides the pattern that actually matters, which is almost always that four endpoints out of two hundred account for most of the errors, and they are the four that look like each other.

Failure modes to plan for

Schema drift. This is the dominant one, and it has no clever fix. A fine-tuned model has partly memorized the catalogue it trained against, so when a field is renamed or a new required argument appears, the model does not error, it produces confident, well-formed, wrong calls. Internal APIs change on a release cadence that has nothing to do with your training schedule. Version tool definitions alongside checkpoints, keep the regeneration pipeline runnable on demand rather than as a one-off, and treat the adapter as a build artifact of a specific API version.

Over-triggering. Train mostly on successful calls and you teach the model that calling is always correct. It starts reaching for a tool when the answer is three messages up in the conversation. Counter it in the data, with explicit examples where the right move is to answer directly or to ask a clarifying question.

Data that encodes the past. Logs record what your systems did, including through deprecated endpoints and workarounds that were retired last quarter. Filter log-derived data against the current specification before training on it, or you will faithfully teach the model habits your platform team spent a year removing.

Real values in captured arguments. Calls carry account identifiers, client names, position sizes, and file paths. A dataset built from logs inherits every one of those, which makes it as sensitive as the systems it was drawn from and subject to the same controls. Treat it accordingly from the first extraction, not at review time.

Grading yourself with your own generator. If your evaluation set is synthesized by the same pipeline as your training set, it shares the pipeline's blind spots and will report a number that production does not reproduce. Keep a held-out set drawn from real requests, however small.

Why this one is a self-hosted advantage

Most arguments for private AI infrastructure are about the data you train on. This one is different, because the sensitive artifact is the tool catalogue itself.

A complete set of your internal tool schemas is a machine-readable map of how your firm is built: what systems exist, what they are called, what they accept, which ones talk to which, and, from the argument names alone, what your data model looks like. Add the training examples and you have added live values from those systems and the sequences in which your people actually use them. It is a more concentrated description of the business than most documents that go through legal review, and fine-tuning requires assembling it in one place, in one file, in a format designed to be read by a machine.

There is a second, more mundane reason this work wants to live inside your walls. Retraining is not a one-time event here. It is tied to your platform release cadence, because that is what schema drift is, and a loop you have to run every few weeks needs to be cheap, fast, and yours. Owning the pipeline turns a schema change into a short job that regenerates the affected slice of the dataset and swaps a LoRA adapter. Renting it turns the same change into a procurement conversation.

Where this fits at Numerata

P95 is the training layer this work runs on: many small runs rather than one large one, grouped and tracked so you can tell which change to the dataset moved which rung of the ladder, with the regeneration and rejection-sampling loops as ordinary jobs rather than scripts on somebody's laptop. It draws GPUs from the pool Lupine manages and hands finished adapters to NinetyFive to serve, which is also where the tool-call parser lives, so the format you trained against and the format you parse are configured in the same place. All of it installs inside your own environment: private cloud, on-premises, or fully air-gapped. The schemas, the call logs, and the weights never leave.

If you are working through the neighbouring questions: agentic fine-tuning covers what happens once single calls are reliable and you need the model to string them together over many turns, synthetic data for fine-tuning goes deeper on building the dataset itself when there is no labelled corpus to start from, and SFT vs. RL covers how to choose a training objective once you have a grader. For the task set that grader runs against, which is the only honest way to tell whether any of this worked, see evaluating agents. For the ground floor, see what fine-tuning is, and for the constraint that shapes all of this, air-gapped LLM training.

Numerata runs inside your own environment: private cloud, on-prem, or fully air-gapped.  ·  Back to blog