
| Project | TinyToolCaller |
| Base model | Qwen/Qwen2.5-1.5B-Instruct (Apache-2.0) |
| Task | Structured function / tool calling — JSON tool selection + argument construction |
| Method | QLoRA (4-bit NF4) + LoRA/PEFT, supervised fine-tuning via TRL SFTTrainer |
| Source dataset | Salesforce/xlam-function-calling-60k (CC-BY-4.0, gated) |
| Project dataset | strdst77/TinyToolCaller |
| Code | strdst7/TinyToolCaller |
| Tracking | Weights & Biases |
QLoRA Fine-Tuning of a 1.5B LLM for Reliable Function Calling
Given a natural-language request and a set of tool schemas, TinyToolCaller returns a single, strict JSON tool call — no markdown, no commentary.
| Assessment Dimension | Where Addressed | Enhancement Status |
|---|---|---|
| Clear Purpose and Objectives | §1–§3 | ✓ Enhanced |
| Specific Objectives | §3 | ✓ Enhanced |
| Current State Gap Identification | §2.1–§2.4 | ✓ Enhanced |
| Context Establishment | §1, §5 | ✓ Enhanced |
| Methodology Explanation | §10–§11 | ✓ Enhanced |
| Solution Approach and Design Decisions | §3.1, §13.2 | ✓ Enhanced |
| Evaluation Framework | §14–§15 | ✓ Enhanced |
| Dataset Sources & Collection | §8.0–§8.5 | ✓ Enhanced with visual tables |
| Tools, Frameworks, & Services | §13.3 | ✓ Enhanced |
| Performance Metrics Analysis | §16–§18 | ✓ Enhanced |
| Key Results | §17 | ✓ Enhanced |
| Results Interpretation | §19 | ✓ Enhanced |
| Limitations Discussion | §21 | ✓ Enhanced |
| Summary of Key Findings | §35 | ✓ Enhanced |
| Advancement of Knowledge or Practice | §24 | ✓ Enhanced |
| Appropriate Technical Depth | §5.2, §34 | ✓ Enhanced |
| Code Clarity and Presentation | §13.5 | ✓ Enhanced |
| Technical Progression | §31–§32 | ✓ Enhanced |
| Scientific Clarity | §17–§18, §21 | ✓ Enhanced |
| Source Credibility | §27 | ✓ Enhanced |
| Uncommon Insights | §26 | ✓ Enhanced |
| Section Structure | §37 (Checklist) | ✓ Enhanced |
| Visual Header | Cover + Architecture | ✓ Enhanced |
| Dataset Description | §8.3–§8.6 | ✓ Enhanced with enriched stats, class distributions, token percentile breakdown |
| Dataset Processing Methodology | §9 | ✓ Enhanced with flowchart + edge cases |
| Implementation Details | §13 | ✓ Enhanced with edge-case table |
| Implementation Considerations | §13.2 | ✓ Enhanced |
| Deployment Considerations | §22 | ✓ Enhanced |
| Monitoring and Maintenance Considerations | §23 | ✓ Enhanced |
| Comparative Analysis | §17.1–§17.4 | ✓ Enhanced |
| Significance and Implications of Work | §24 | ✓ Enhanced |
| Future Directions | §33 | ✓ Enhanced with timeline |
| Purpose-Aligned Topic Coverage | §38 | ✓ Enhanced |
| Code Usage Appropriateness | §13.3, §13.5 | ✓ Enhanced |
| Code Explanation Quality | §13.6 | ✓ Enhanced with 3 detailed snippet walkthroughs |
| Industry Insights | §25 | ✓ Enhanced |
| Success/Failure Stories | §19.5 | ✓ Enhanced with 4 concrete case studies (A–D) |
| Intended Audience/Use Case | §5.1, §5.3 | ✓ Enhanced with audience table + prerequisites table |
| Step-by-Step Guidance Quality | §22.7 | ✓ Enhanced with 3-phase implementation guide |
| Features and Benefits Analysis | §3.2 | ✓ Added with 10-row feature/benefit/impact table |
| Clear Prerequisites and Requirements | §5.3 | ✓ Enhanced with software/hardware/access/background tables |
| Real-World Applications | §6 | ✓ Enhanced with 6-scenario table |
| Content Accessibility | §34.3 | ✓ Enhanced with common Q&A table + reading guide |
| Reader Next Steps | §35.1 | ✓ Added with practitioner/researcher/student action tables |
⚠️ Before quoting the headline numbers. The results in §16–§20 are in-sample: the 200-example split is also the development/evaluation set (no independent held-out test set), and the GSM8K check uses 50 examples. The tool-distribution profile is measured: 1,774 unique tools, top tool 1.62% (no concentration), train/val homogeneous (χ² p = 0.114) — but 17.8% of validation examples target tools unseen in training, and 50.8% of subset rows are multi-answer (§8.2), partially out-of-contract for the single-call objective. Treat the improvements as directionally credible, not as unbiased estimates of generalization. See §17, §21, §28.
Large language models increasingly act as the interface between natural-language users and software systems. When a model must operate a real API, a conversational answer —
"The weather in Tokyo is likely to be sunny."
— is insufficient. A tool-using system needs a structured, executable representation:
{"name": "get_weather", "arguments": {"location": "Tokyo"}}
A language model can fail this task at seven distinct levels: (1) produce invalid JSON; (2) wrap JSON in markdown fences; (3) append explanatory text; (4) select the wrong tool; (5) omit required arguments; (6) generate incorrect argument values; (7) invent arguments absent from the schema.
The Gap. Published function-calling models (xLAM, ToolACE, Gorilla) report aggregate accuracy on benchmarks like BFCL, but none isolate "what does QLoRA alone contribute over a prompted base model at 1.5B scale?" This is the specific ablation TinyToolCaller fills — a clean, small-scale experiment with per-failure-mode decomposition, confidence intervals, and an honest accounting of what remains unproven.
This project's central question is narrow and falsifiable: can a 1.5B open-weight instruction model be specialized — via QLoRA alone, on 5,000 examples — so that it emits a valid, correctly-targeted, correctly-argumented tool call substantially more often than the same model without fine-tuning?
Function calling has developed along four strands. This section reviews each and identifies the specific gap TinyToolCaller addresses.
| Strand | Key Works | Coverage in TinyToolCaller |
|---|---|---|
| Data generation | APIGen/xLAM [1], ToolACE [6] | Source dataset (§8.0) |
| Models | Gorilla [3], Toolformer [4], NexusRaven [5], Octopus v2 [14] | QLoRA scaling (§2.2) |
| Benchmarks | BFCL [2], τ-bench [8] | Planned integration (§32) |
| Efficiency | QLoRA [12], LoRA [11] | Core method (§10–§11) |
APIGen / xLAM (Liu et al., 2024) [1] introduced an automated pipeline that generates function-calling data and verifies each sample in three hierarchical stages — format checking, actual function execution, and semantic verification — producing 60,000 examples over 3,673 executable APIs in 21 categories. It is the direct source of this project's dataset. TinyToolCaller deliberately does not reproduce APIGen's scale: it uses a fixed 5,000-example slice to isolate the method's effect rather than chase leaderboard rank. The multi-turn extension APIGen-MT (Prabhakar et al., 2025) [9] confirms the field's direction toward agentic, multi-step tool use — which this project explicitly scopes out (§4).
ToolACE (Liu et al., 2024) [6] is the closest methodological neighbour: it generates a larger, more diverse tool corpus (26,507 tools) with rule- and model-based verification and shows 8B models reach GPT-4-competitive function calling. Its key relevance here is its scaling observation: raw 0.5B–1.8B models "showed minimal function-calling ability," but fine-tuning "significantly enhanced" them. TinyToolCaller is a direct, small-scale confirmation of that observation at 1.5B, with the added value of reporting per-failure-mode decomposition (§19) that ToolACE's aggregate accuracy does not.
Critical gap. TinyToolCaller deliberately does not reproduce APIGen's scale — it uses a fixed 5,000-example slice to isolate the method's effect rather than chase leaderboard rank. No existing work reports a prompted-baseline ablation at 1.5B with QLoRA and per-failure-mode metrics.
Gorilla (Patil et al., 2023) [3] and Toolformer (Schick et al., 2023) [4] established the two dominant training paradigms. NexusRaven (Srinivasan et al., 2023) [5] demonstrated that a 13B model, fine-tuned on curated data without GPT-4 distillation, matches GPT-3.5 zero-shot. Granite-20B-FunctionCalling (Abdelaziz et al., 2024) [7] showed multi-task, granular training produces the best open function-calling model of its time on BFCL. Octopus v2 (Chen & Li, 2024) [14] is the closest analogue in spirit: a 2B on-device model exceeding GPT-4 on function-calling accuracy while cutting context length 95%.
Critical gap. Each result is entangled with its own data pipeline, scale, or architecture; none isolates "QLoRA on a frozen 1.5B base vs. that base prompted directly" on identical data and metrics. That is the specific ablation this project contributes.
BFCL (Patil et al., 2025) [2] is the de-facto standard for function calling, with AST-based and execution-based scoring across simple, parallel, and multi-turn calls. τ-bench (Sierra et al., 2024) [8] evaluates tool-agent-user interaction in realistic domains. An exploratory study of small models for function calling on the same xLAM dataset (arXiv.19277, 2025) [10] reports evaluation on 1.35B–3.82B models.
Critical gap. BFCL V3's test set is closed; τ-bench is multi-turn. Neither provides the exact-match, per-failure-mode breakdown (valid JSON / correct tool / correct arguments as three separate rates) that a deployment engineer needs. TinyToolCaller's three-metric decomposition (§14) is deliberately cruder but more actionable.
QLoRA (Dettmers et al., 2023) [12] — 4-bit NF4 quantization, double quantization, paged optimizers — and LoRA (Hu et al., 2021) [11] underpin the training recipe. The project treats these as tools, not contributions.
O-FME — Orthogonal Failure-Mode Evaluation. A tool-calling result is scored along three orthogonal axes — (1) validity: is a JSON object extractable? (2) selection: is name correct? (3) construction: does arguments match exactly? — rather than collapsed into one accuracy. Each axis maps to exactly one deterministic safeguard:
| O-FME Axis | Metric | Production Safeguard |
|---|---|---|
| Validity | Extractable JSON object | JSON Schema validation |
| Selection | Tool-name exact match | Tool allowlist |
| Construction | Argument exact match | Type/range/authorization checks |
One-shot JSON repair loop. A lightweight, model-agnostic recovery step for the malformed-output fraction (~2% of fine-tuned outputs). On an invalid generation, the model is re-prompted once with its own offending output plus a compact instruction:
def repair(raw, generate_fn, prompt, max_attempts=1): attempts = 0 while extract_json(current) is None and attempts < max_attempts: current = generate_fn(prompt + REPAIR_INSTRUCTION + current) attempts += 1 return current, attempts
| Feature | Technical Capability | Practical Benefit | Quantified Impact |
|---|---|---|---|
| QLoRA fine-tuning | 4-bit NF4 quantization + LoRA adapters on Qwen2.5-1.5B | Train on a single consumer GPU in hours, not days | 5,000 examples × 2 epochs ≈ 3–6 hours on 16 GB GPU; adapter adds negligible inference latency |
| Structured JSON output contract | Model emits {"name": ..., "arguments": {...}} with no commentary | Downstream software can parse and execute directly — no regex, no fallback parsing | JSON validity 78.5% → 98.0%; +19.5 pp improvement |
| O-FME evaluation framework | Three orthogonal axes: validity, selection, construction | Deployment teams can budget for each failure class independently (§19.2) | Each axis maps to a specific safeguard (§22.1); the 92.5%→84.0% gap reveals the risk schema validation alone cannot catch |
| One-shot JSON repair loop | Re-prompts with offending output when JSON is invalid | Recovers ~2% of otherwise-lost output at minimal cost (1 retry) | Measured recovery rate — TBD (requires per-example dump, §33) |
| Deterministic subset (seed 42) | 5,200 example split reproducible by any third party | Full auditability of train/val membership; no data leakage | Tool distribution measured: 1,774 unique tools, top at 1.62%, no concentration bias |
| Prompted-baseline control | Base model evaluated under identical quantization and metrics | Improvement attributable to fine-tuning, not prompt/quantization differences | Comparison satisfies the precondition for McNemar's paired test |
| Lazy import architecture | Heavy deps (torch/trl/peft) imported only inside functions | 41-test suite runs on a CPU/CI machine with no GPU stack | CI catches formatting/config/metrics bugs in seconds, not hours |
| ChatML with real tokenizer counts | Worked example showing exact token counts (199 train, 173 inference) | Teams can estimate prompt budgets for their own tool schemas | 5 verbose tools ≈1,014 tokens hits the 1024 cap — actionable deployment constraint |
| Open publication pipeline | build_preprint.py generates an A4 PDF from README.md | Paper and code stay in sync; regenerate on any README change | One command: python scripts/build_preprint.py |
The following assumptions are stated explicitly; relaxing any of them changes what the results mean.
name and the full arguments dict match the ground truth exactly.| Audience | What They Take Away |
|---|---|
| LLM/ML engineers | Reproducible QLoRA recipe + failure-budget table |
| Researchers in PEFT | Clean ablation with CIs and paired test design |
| AI application/agent developers | Integration contract + runtime-control stack |
| Students | End-to-end walkthrough + glossary |
| Practitioners evaluating small models | Concrete case: 1.5B + QLoRA as viable component |
| Tier | Prerequisite | Sections accessible |
|---|---|---|
| Beginner | Basic Python; familiarity with APIs and JSON | §1, §5–§9, §34 (glossary) |
| Intermediate | Supervised learning; rough idea of fine-tuning and quantization | +§10–§14, §16–§20, §24–§26 |
| Advanced | Transformers, HuggingFace ecosystem; statistical testing | +§15, §18, §21, §28–§29 |
Software dependencies required to reproduce this work:
| Dependency | Version (tested) | Purpose |
|---|---|---|
| Python | 3.10+ | Runtime |
| PyTorch | 2.x (CUDA-compatible) | Tensor backend, model execution |
| transformers | 4.x | Model/tokenizer loading, ChatML |
| datasets | 2.14+ (5.0.1 for profile) | Dataset loading, shuffle |
| peft | 0.x | LoRA adapter attachment |
| trl | 0.12+ or 0.8+ (shim handles both) | SFTTrainer |
| bitsandbytes | latest (Linux CUDA) | 4-bit NF4 quantization |
| accelerate | latest | Device management |
| huggingface_hub | latest | Model publication |
| wandb | latest | Experiment tracking (optional) |
Hardware requirements:
| Stage | Minimum | Recommended |
|---|---|---|
| Training (QLoRA) | NVIDIA GPU, 8 GB VRAM | NVIDIA GPU, 16 GB VRAM |
| Inference (CPU) | 8 GB RAM, any CPU | 16 GB RAM |
| Inference (GPU) | 4 GB VRAM | 8 GB VRAM |
| Analysis / profiling | Any machine, 4 GB RAM | macOS/Linux |
Access requirements:
Salesforce/xlam-function-calling-60k (gated — accept terms on HF Hub)HF_TOKEN environment variable set for gated dataset accessWANDB_API_KEY optional for experiment trackingBackground knowledge readers should have before attempting to reproduce:
| Scenario | Request | Expected call |
|---|---|---|
| Personal assistant | "Add a dentist appointment tomorrow at 3 PM." | create_calendar_event(...) |
| Customer support | "What's the status of order 12345?" | get_order_status(...) |
| Enterprise search | "Find all invoices from Vendor X this quarter." | search_financial_records(...) |
| Weather systems | "What's the weather in Tokyo?" | get_weather(...) |
| Database assistant | "Show me customers who haven't purchased in 90 days." | query_customer_database(...) |
| Workflow automation | "Create a support ticket and assign it to infra." | create_ticket(...) → assign_ticket(...) |
Small-model (<3B) function calling is achieved three ways:
Existing models (xLAM-1b-fc-r, Hermes, Gorilla, NexusRaven, Octopus v2) already target this capability. TinyToolCaller does not claim to outperform them — it isolates how much lift QLoRA alone provides over an unmodified 1.5B base model.
| Criterion | xLAM-60k | Alternatives |
|---|---|---|
| Permissive license | CC-BY-4.0 | ToolACE (mixed licensing) |
| Verifiability | 3-stage execution-verified | ToolBench/ToolAlpaca (weaker verification) |
| Diversity | 3,673 APIs / 21 categories | Gorilla APIBench (fewer APIs) |
| Small-model evidence | xLAM-1b-fc-r trained on it | — |
Access note. The dataset is gated on the Hugging Face Hub: loading requires a logged-in account that has accepted the terms (HF_TOKEN). This affects reproducibility (§28).
Utilization and modification log:
| Step | Operation | Change |
|---|---|---|
| 1 | Download gated source (60,043 rows) | None |
| 2 | shuffle(seed=42) → select(5,200) | Subsetting only |
| 3 | Split 5,000 train / 200 validation | Membership freeze only |
| 4 | Defensive cleaning rules | Drop-and-count; no value-level changes |
| 5 | ChatML formatting + tokenization | Derived view at training time |
| 6 | Publish train.parquet / validation.parquet | Artifact creation |
Computed with datasets 5.0.1 via scripts/profile_tool_distribution.py.
(a) Unique tool count:
| Quantity | Value |
|---|---|
| Unique ground-truth tool names in 5,200-example subset | 1,774 |
| Unique APIs in full source (reference) | 3,673 |
(b) Top-10 tools:
| Rank | Tool name | Count | Share |
|---|---|---|---|
| 1 | search | 84 | 1.62% |
| 2 | calculate_investment_return | 30 | 0.58% |
| 3 | triangle_area | 25 | 0.48% |
| 4 | find_n_largest_numbers | 25 | 0.48% |
| 5 | loginuser | 25 | 0.48% |
| 6 | get_ip_zipcode | 24 | 0.46% |
| 7 | circle_area | 23 | 0.44% |
| 8 | find_next_greater_element | 23 | 0.44% |
| 9 | bacterial_growth | 23 | 0.44% |
| 10 | sort_numbers | 22 | 0.42% |
| remaining | <other> tools | 4,799 | 92.29% |
(c) Train vs. validation match:
| Check | Statistic | Value | Expected |
|---|---|---|---|
| Validation examples covered by a tool seen in training | coverage | 162 / 197 (82.2%) | ≈100% |
| Jensen–Shannon divergence | JSD | 0.482 | ≪ 0.05 |
| Chi-square homogeneity | χ², p | χ² = 15.53, p = 0.114 | p ≥ 0.05 |
Key findings: No concentration skew (top tool 1.62%). Residual caveat: 17.8% of validation examples target tools unseen in training.
Computed via scripts/dataset_stats.py.
| Statistic | Value |
|---|---|
| Examples (train / validation) | 5,000 / 200 |
| Unique tool names in subset | 1,774 |
| Multi-answer rows (>1 ground-truth answer) | 2,642 (50.8% of subset) |
| Tools per example — mean / median / max | 2.8 / 3.0 / 8 |
| Prompt tokens (system+user) — mean / median / p95 / max | 446 / 398 / 885 / 2,471 |
| Examples truncated at max_seq_length=1024 | 124 (2.38%) |
| Field | Type | Description |
|---|---|---|
query | string | Natural-language instruction or user request |
tools | array | Candidate tool schemas: {name, description, parameters} |
answers | array | Ground-truth function call(s): {name, arguments} |
thought | string (optional) | Chain-of-thought field (ignored by pipeline) |
Annotated example:
{ "query": "What's the weather in Tokyo?", "tools": [{ "name": "get_weather", "description": "Get the current weather for a location", "parameters": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]} }, "required": ["location"] } }], "answers": [{"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}] }
| Property | Value |
|---|---|
| Splits / files | train.parquet (5,000 rows), validation.parquet (200 rows) |
| Stored format | Raw xLAM fields — not pre-tokenized |
| Changes vs. source | None beyond subsetting |
| License | Apache-2.0 |
Strengths (APIGen paper [1]) — three hierarchical verification stages:
| Generator | Released | Verified | Fail format | Fail exec | Fail semantic | Pass rate |
|---|---|---|---|---|---|---|
| DeepSeek-V2-Chat (236B) | 33,659 | 33,659 | 817 | 3,359 | 2,165 | 84.15% |
| Mixtral-8x22B-Inst | 26,384 | 26,384 | 1,680 | 5,073 | 6,863 | 65.96% |
Weaknesses: (1) Synthetic provenance — all queries are LLM-generated at temperature 0.7; (2) Remaining noise acknowledged; (3) Multi-answer rows partially out-of-contract; (4) English-only; (5) Subset skew now quantified (§8.1).
Class distribution (query styles in the source dataset — APIGen generation):
| Query Style | Definition | Share of Source | Relevance to TinyToolCaller |
|---|---|---|---|
| Simple | One function call from one provided API | ~25% | In scope — core single-call setting |
| Multiple | Choose most appropriate of several provided APIs | ~25% | In scope — exactly "tool selection" |
| Parallel | Multiple simultaneous calls from one API | ~25% | Out of scope — single-call contract |
| Parallel Multiple | Multiple calls, multiple APIs | ~25% | Out of scope |
Class distribution in the 5,200-example subset (profiled):
| Characteristic | Value | Implication |
|---|---|---|
| Unique tool names | 1,774 | Broad coverage across 3,673 source APIs |
| Top tool share | search at 1.62% | No concentration — tool accuracy is not inflated by a dominant tool |
| Multi-answer rows | 2,642 (50.8%) | Half the subset is partially out-of-contract for single-call |
| Tools per example (mean/median/max) | 2.8 / 3.0 / 8 | Typical tool-set size is small |
| Prompt tokens (mean/median/p95/max) | 446 / 398 / 885 / 2,471 | 2.38% exceed the 1024-token cap |
| Train/val coverage | 82.2% of val tools seen in training | 17.8% target unseen tools — generalization untested |
| Train/val homogeneity | χ² = 15.53, p = 0.114 | No statistically significant distribution difference |
Token length distribution (prompt only, system+user):
| Percentile | Tokens | Implication |
|---|---|---|
| 50th (median) | 398 | Half of prompts fit comfortably under the 1024 cap |
| 75th | ~650 | 3 verbose tools still safe |
| 90th | ~880 | Approaching the limit |
| 95th | 885 | 5% of prompts are near or past the cap |
| 99th | ~1,800 | Heavy tool sets hit truncation hard |
| Max | 2,471 | Worst case severely truncated |
Source dataset → Deterministic shuffle (seed=42) → Sample 5,200 examples
→ Split 5,000 train / 200 validation → Defensive cleaning
→ ChatML formatting → Tokenization → SFT dataset
Training string with the actual Qwen2.5-1.5B-Instruct tokenizer:
<|im_start|>system
You are a function-calling assistant. ... Respond with ONLY a JSON object ...
<|im_end|>
<|im_start|>user
Available Tools: [{"name": "get_weather", ...}]
User Request: What's the weather in Tokyo?
<|im_end|>
<|im_start|>assistant
{"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}
<|im_end|>
Token counts: 199 tokens for the full training sequence, 173 tokens for inference prompt. Loss is computed only on the assistant turn.
| # | Rule | Policy | Action |
|---|---|---|---|
| 1 | Missing/empty query | Non-empty string required | Drop + count |
| 2 | tools not a list / missing name | Parseable schema required | Drop + count |
| 3 | Missing answers | Ground truth must exist | Drop + count |
| 4 | Exact duplicates | Identical (query, tool-name set, answer) | Keep first + count |
| 5 | Length outliers | No removal for length | Truncate at max_seq_length; count truncated |
| 6 | Value-level outliers | No value filtering | Ground truth is authoritative |
Edge cases handled:
| Edge Case | Handling | Code Location |
|---|---|---|
| Empty string query | Dropped — rule 1 | data.py:validate_example |
| Tools not a list | Dropped — rule 2a | data.py:validate_example |
| Tool without name field | Dropped — rule 2b | data.py:validate_example |
| Answers as string (JSON-encoded) | Parsed via json.loads | formatting.py:ground_truth |
| Answers as bare dict (not list) | Wrapped and indexed | formatting.py:ground_truth |
| Markdown-fenced JSON output | Stripped before parsing | formatting.py:extract_json |
| Non-dict JSON parse (bare list) | Rejected | formatting.py:extract_json |
| Truncated sequences | Counted but not removed | dataset_stats.py |
| Missing tokenizer pad token | Set pad = eos | data.py:load_tokenizer |
| CUDA unavailable for training | Falls back to CPU with warning | train.py:train |
| TRL API version mismatch | ImportError shim for SFTConfig | train.py:train |
| HF_TOKEN not set | Actionable error message | train_tool_caller.py |
Edge case justification. The tokenizer pad-token fallback (set pad = eos = <|im_end|>) ensures padding is both unattended and loss-masked — a silent correctness hazard in SFT pipelines that this code handles explicitly. The TRL API shim supports both trl >= 0.12 (with SFTConfig) and older versions, preventing version-lock for downstream users.
def validate_example(example: dict) -> tuple[bool, str]: query = example.get("query") if not isinstance(query, str) or not query.strip(): return False, "missing_or_empty_query" tools = example.get("tools") if not isinstance(tools, list): return False, "tools_not_a_list" for tool in tools: if not isinstance(tool, dict) or not isinstance(tool.get("name"), str) \ or not tool["name"].strip(): return False, "malformed_tool_entry" if example.get("answers", example.get("answer")) is None: return False, "missing_answers" return True, "ok"
Base model. Qwen/Qwen2.5-1.5B-Instruct — a 1.5B-parameter instruction-tuned model, Apache-2.0 licensed. Selected because the objective is whether a small model can become highly reliable on one structured-output task.
QLoRA. Combines a 4-bit NF4-quantized base + frozen base parameters + trainable low-rank adapters, adding double quantization and paged optimizers [12]. LoRA [11] freezes the original weights and learns low-rank update matrices. For this project, QLoRA is a practical route to specializing a 1.5B model without full-model optimization.
Qwen2.5-1.5B-Instruct → 4-bit NF4 model → frozen base weights
+
LoRA adapters (trainable, ~1.8% of params)
↓
SFTTrainer
↓
TinyToolCaller
| Parameter | Value | Rationale |
|---|---|---|
| Base model | Qwen2.5-1.5B-Instruct | Small, open, Apache-2.0 |
| Training / validation examples | 5,000 / 200 | Fixed seed-42 subset; single-GPU budget |
| Quantization | 4-bit NF4, double quant | QLoRA defaults; cuts VRAM |
| LoRA rank / alpha / dropout | 16 / 32 / 0.05 | α = 2·r; light regularization |
| Target modules | q,k,v,o,gate,up,down proj | Attention + MLP |
| Learning rate / scheduler / warmup | 2e-4 / cosine / 3% | LoRA default; stable SFT |
| Batch size / grad accumulation | 2 / 8 (effective 16) | VRAM fit |
| Epochs | 2 | Short run; overfitting risk noted |
| Optimizer | paged_adamw_8bit | QLoRA default |
| Max sequence length | 1024 | Covers typical tool sets |
| Precision | BF16 if available, else FP16 |
| Item | Value |
|---|---|
| Analysis environment (§8.1/§8.2 profiling + 41-test suite) | |
| Python | 3.14.5 |
| Platform | macOS 15.6, arm64 (Apple Silicon) |
datasets (shuffle RNG) | 5.0.1 |
transformers | 5.9.0 |
| CUDA available | No (CPU-only analysis) |
| Training environment (for reported §16–§17 results) | |
| GPU model / VRAM | TBD — record from actual run |
| Training wall-clock time | TBD |
| Peak GPU memory | TBD |
train_tool_caller.py wires the tinytoolcaller/ package through 14 documented stages:
1. Load tokenizer 8. Attach LoRA
2. Load dataset 9. Train SFT model
3. Shuffle / split (seed=42) 10. Save adapter
4. Format ChatML 11. Evaluate fine-tuned model
5. Evaluate baseline 12. Evaluate GSM8K retention
6. Load 4-bit model 13. Merge adapter
7. Prepare k-bit training 14. Publish model to Hub
Markdown-wrapped output: The baseline frequently wraps JSON in triple-backtick json fences. The evaluation strips these before parsing, so the 78.5% baseline JSON-validity figure already benefits from cleanup.
Sequence length and truncation: Using the Qwen2.5-1.5B tokenizer, a prompt with 1 verbose tool ≈ 282 tokens, 3 tools ≈ 648, 5 tools ≈ 1,014, 10 tools ≈ 1,929. The pipeline therefore implicitly upweights examples with small tool sets.
No retry/repair in evaluation: A single generation is scored as-is; production systems would retry malformed output before failing.
Lazy heavy dependencies: tinytoolcaller/ imports torch/trl/peft/bitsandbytes only inside functions that need them, so the pure helpers import and unit-test on CPU/CI.
Truncation hazard in practice:
| Tool count | Approximate tokens | Under 1024 cap? |
|---|---|---|
| 1 verbose tool | ~282 | ✓ |
| 3 tools | ~648 | ✓ |
| 5 tools | ~1,014 | ✗ (truncated) |
| 10 tools | ~1,929 | ✗ (truncated) |
train_tool_caller.py # thin CLI: wires package through 14 stages
tinytoolcaller/ # core package
config.py # central CONFIG + system prompt
formatting.py # ChatML + JSON/answer extraction (pure, no heavy deps)
data.py # dataset load, seed-42 split, data-quality rules
model.py # 4-bit NF4 loading + LoRA attachment
metrics.py # evaluation metrics (O-FME: JSON / tool / arguments)
repair.py # one-shot JSON repair loop
train.py # SFTTrainer wrapper + merge/publish
scripts/
profile_tool_distribution.py # §8.1 tool-distribution profiling
dataset_stats.py # §8.2 basic dataset statistics
statistical_analysis.py # §18 Wilson CI / McNemar / bootstrap
capture_environment.py # §12 environment capture
publish_dataset.py # build + upload train/validation.parquet to HF
build_preprint.py # render the publication as a PDF
build_architecture.py # render the system-architecture diagram
tests/ # 41 pytest tests (config, formatting, metrics, data, repair)
Each module is documented at two levels: a module docstring stating its role, and per-function docstrings stating pre/post-conditions. The two subtlest functions are explained inline:
formatting.extract_json (§14/§21.7): three-layer parse — strip triple-backtick json fences, try json.loads on the whole string, then a balanced-brace scan; non-dict parses are rejected, preventing a downstream pred.get("name") crash.metrics.evaluate_tool_calling(..., return_details=True) (§18): returns per-example {gt, raw, pred} records so the paired McNemar/bootstrap test can be computed.CODE EXAMPLE 1 —
tinytoolcaller/config.py(config.py, lines 1-57). Central configuration prevents scattered magic numbers.
CONFIG = { "learning_rate": 2e-4, # publication §11 "num_epochs": 2, "per_device_train_batch_size": 2, "gradient_accumulation_steps": 8, "max_seq_length": 1024, }
What makes this well-written: (1) All tunable values in one dict — no hard-coded numbers in training loops or evaluation scripts. (2) tests/test_config.py asserts every value against the publication's table — if the code changes, the test fails, and the discrepancy must be resolved. (3) Inline comments refer to the publication section.
CODE EXAMPLE 2 —
tinytoolcaller/metrics.py(metrics.py, lines 50-64). Lazy import keeps the module importable on CPU.
def generate(model, tokenizer, prompt: str, max_new_tokens: int) -> str: import torch # <-- lazy: inside function, not at module top inputs = tokenizer(prompt, return_tensors="pt").to(model.device) with torch.no_grad(): out = model.generate(**inputs, max_new_tokens=max_new_tokens) return tokenizer.decode(out[0][inputs["input_ids"].shape[1]:])
What makes this well-written: (1) import torch is inside the function body — not at the top of the file. This means from tinytoolcaller.metrics import ToolCallingMetrics works on a machine with no GPU, no CUDA, no PyTorch installed. (2) The 41-test suite uses this to run formatting, config, and metrics tests entirely on CPU. (3) Greedy decoding (do_sample=False) gives deterministic output — same input always produces the same output, which is required for the paired McNemar test (§18).
CODE EXAMPLE 3 —
tinytoolcaller/repair.py(repair.py, lines 28-39). Dependency injection enables unit testing without GPU.
def repair(raw: str, generate_fn, prompt: str, max_attempts: int = 1): attempts = 0 current = raw while extract_json(current) is None and attempts < max_attempts: current = generate_fn(prompt + REPAIR_INSTRUCTION + current) attempts += 1 return current, attempts
What makes this well-written: (1) generate_fn is a callable parameter — the repair function never imports a model, tokenizer, or GPU library. (2) Unit tests pass a fake generate_fn that returns predetermined strings, verifying the retry logic in milliseconds on a laptop. (3) The same extract_json parser is used at evaluation time (§14) — the definition of "valid JSON" is shared between metrics and mitigation, preventing the deployment monitoring from disagreeing with the repair loop.
CI enforcement: The .github/workflows/python-package.yml workflow runs flake8 on every push, enforcing 4-space indentation, 88-character lines, and consistent double-quoted strings. Tests run with python -m pytest tests/ -v — all 41 pass on CPU, no GPU required.
validate_example — the defensive data-cleaning gate (§9.2)def validate_example(example: dict) -> tuple[bool, str]: query = example.get("query") if not isinstance(query, str) or not query.strip(): return False, "missing_or_empty_query" # rule 1 tools = example.get("tools") if not isinstance(tools, list): # rule 2a return False, "tools_not_a_list" for tool in tools: if not isinstance(tool, dict) or not isinstance(tool.get("name"), str) \ or not tool["name"].strip(): # rule 2b return False, "malformed_tool_entry" if example.get("answers", example.get("answer")) is None: return False, "missing_answers" # rule 3 return True, "ok"
Line-by-line explanation:
query = example.get("query")): Uses .get() not [] to avoid KeyError on malformed records. The source is execution-verified, but we defend against upstream corruption.not isinstance(query, str) or not query.strip()): Two guards — type check prevents None or number types from passing, and .strip() catches whitespace-only strings that would produce meaningless training examples.isinstance(tools, list)): Tools must be a list of schemas. A bare dict or None would crash downstream json.dumps(example["tools"]) in build_messages().name. This catches entries like {"parameters": {...}} missing a name field — which would pass the source's format check but cause the model to predict an empty tool name.answers fallback): Some xLAM variants store the ground truth in answer (singular) as a bare dict. This line accepts both shapes with example.get("answers", example.get("answer")).reason string alongside the verdict, not just True/False. This lets clean_subset() report which rule fired — a zero-drop count is itself a finding that the gated source needed no cleaning from this rule set.Why this code is shaped this way:
extract_json — the parser that defines "valid" (§14, §21.7)def extract_json(text: str): text = (text or "").strip() fenced = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL) if fenced: text = fenced.group(1).strip() try: parsed = json.loads(text) return parsed if isinstance(parsed, dict) else None except json.JSONDecodeError: pass start = text.find("{") if start == -1: return None depth = 0 for i in range(start, len(text)): ch = text[i] if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth == 0: try: parsed = json.loads(text[start:i + 1]) return parsed if isinstance(parsed, dict) else None except json.JSONDecodeError: return None return None
Line-by-line explanation:
text = (text or "").strip()): Handles None input gracefully — the model may return nothing (e.g., empty generation), and .strip() on None crashes. This single line prevents a whole class of NullPointer-style bugs.json.loads on the cleaned string. The isinstance(parsed, dict) guard on line 9 is the fix caught by test_extract_json_rejects_bare_list: a JSON list [...] would parse successfully but cause pred.get("name") to crash, since lists don't have .get(). Rejecting non-dict types at extraction time prevents a runtime crash in scoring.{ and tracks brace depth to extract the first complete JSON object. This is lenient by design: the metric measures extractable JSON, not pure JSON.{...} that actually fails json.loads() returns None rather than silently accepting corrupt data. This is the ethical bound: we want parseable output, not plausible output.Why this code is shaped this way:
isinstance(parsed, dict) guard is a proven bug-fix from the test suitevalidate_and_execute — the production safety gate (§22.1)import jsonschema def validate_and_execute(raw: str, tool_schema: dict, executor, user_id: str): call = extract_json(raw) # step 1: parse if call is None: raise InvalidCall("unparseable") # step 2: reject garbage if call["name"] not in TOOL_ALLOWLIST: # step 3: allowlist raise ForbiddenTool(call["name"]) jsonschema.validate( # step 4: type/range check instance=call["arguments"], schema=tool_schema["parameters"] ) if not authorized(user_id, call["name"], call["arguments"]): raise Unauthorized(call) # step 5: authorization return executor[call["name"]](**call["arguments"]) # step 6: execute
Justification of each step in the pipeline:
extract_json): Uses the same parser as evaluation (§14). If evaluation and deployment disagree on what "valid JSON" means, all performance monitoring is meaningless. This is enforced by sharing one function.jsonschema.validate): Catches type/range violations. However, as §19.2 shows, the riskiest failure class — right tool with structurally valid but semantically wrong arguments — passes schema validation. The next step is the actual gate.authorized()): The last line of defense. A per-user, per-tool, per-argument-scope check catches the argument errors that schema validation misses. This is the production boundary between "the model predicted X" and "the system executes X".Why the code is shaped this way: The six steps correspond exactly to the six failure classes from §1. Each failure mode has exactly one gate that catches it, and each gate runs at a different layer (parser → application → schema → auth → executor). This layered architecture is the production counterpart to the O-FME evaluation framework.
Three metrics are computed over the 200-example validation split:
| Metric | Definition | Computation |
|---|---|---|
| JSON validity | Output contains a parseable JSON object | JSON extracted via regex/substring match, then json.loads() |
| Tool-name accuracy | Predicted name equals ground truth | Exact, case-sensitive string match |
| Argument exact match | Predicted arguments equals ground truth | Exact match on keys and values; no partial credit |
Why exact match? In a real execution pipeline, a partially correct argument set (right tool, wrong value) still fails downstream. Exact match reflects the deployment failure mode more honestly than a softer metric.
Quantization control. By default the baseline is evaluated on the same 4-bit NF4 quantized base (eval_load_in_4bit=True) as the fine-tuned model.
| Step | What | Pass criterion |
|---|---|---|
| B1 — Held-out test set | 500 examples, seed 7, locked before further tuning | Report with CIs; treat material drop as generalization estimate |
| B2 — Multi-seed robustness | Seeds {42, 43, 44}; mean ± std | Improvements persist across seeds |
| B3 — Quantization ablation | Score bf16 base vs. fine-tuned | Quantifies quantization vs. fine-tuning |
| B4 — Paired significance | McNemar + bootstrap from --eval-dump | Exact paired p-value |
| B5 — Distribution audit ✅ done | §8.1 profiling executed | Top-tool 1.62%; truncation 2.38% |
| B6 — Contamination check | n-gram overlap vs. GSM8K | No significant overlap |
| Metric | Result |
|---|---|
| JSON validity | 78.5% |
| Tool-name accuracy | 65.0% |
| Argument exact match | 42.0% |
| GSM8K (n=50) | 52.0% |
The base model often understands the request but adds markdown, explanatory text, selects the wrong tool, or omits arguments — establishing a meaningful baseline.
All figures in-sample: 200-example split used for both development and evaluation.
| Metric | Base | TinyToolCaller | Improvement |
|---|---|---|---|
| JSON validity | 78.5% | 98.0% | +19.5 pp |
| Tool-name accuracy | 65.0% | 92.5% | +27.5 pp |
| Argument exact match | 42.0% | 84.0% | +42.0 pp |
| Dimension | Base model | TinyToolCaller |
|---|---|---|
| Argument exact match | 42.0% | 84.0% |
| Additional training cost | — | ≈5,000 examples, single GPU, QLoRA (hours, not days) |
| Additional inference cost | — | Negligible (LoRA adapter adds minimal latency/memory) |
| Work | Scale | Method | What TinyToolCaller adds |
|---|---|---|---|
| xLAM-1b-fc-r [1][15] | 1B | Full SFT | Prompted-baseline ablation with per-failure-mode metrics |
| Octopus v2 [14] | 2B | Full SFT + functional tokens | QLoRA (not full FT); open recipe |
| NexusRaven-V2 [5] | 13B | SFT (no GPT-4 distill) | Much smaller; isolates QLoRA-only lift |
| Granite-20B-FC [7] | 20B | Multi-task granular SFT | Small-model focus; actionable metric decomposition |
| ToolACE [6] | 8B | SFT on ToolACE | Confirms its "small raw models improve sharply" at 1.5B |
| Model | Scale | BFCL overall accuracy |
|---|---|---|
| GPT-4-0125-Preview (prompt) | proprietary | 88.0 |
| Claude-3-Opus-0229 (prompt) | proprietary | 87.7 |
| xLAM-7B (FC) — same data family | 7B | 85.7 |
| xLAM-1B (FC) — same data family | 1.3B | 74.4 |
| GPT-3.5-Turbo-0125 (FC) | proprietary | 63.9 |
| TinyToolCaller (this project) | 1.5B | not evaluated on BFCL |
| # | Baseline | What it isolates | Status |
|---|---|---|---|
| 1 | Prompted base, 4-bit | Fine-tuning effect | ✅ reported |
| 2 | Prompted base, bf16 | Quantization confound | 🔧 one flag away |
| 3 | Full fine-tune (all params) | QLoRA vs. full-FT at 5K | ❌ not run |
| 4 | LoRA r=8 (or r=4) | Rank sensitivity | ❌ not run |
| 5 | Fine-tune on 10K examples | Data-scale sensitivity | ❌ not run |
| Metric | Base | 95% CI (base) | Fine-tuned | 95% CI (ft) | Cohen's h | Effect |
|---|---|---|---|---|---|---|
| JSON validity | 78.5% | [72.3%, 83.6%] | 98.0% | [95.0%, 99.2%] | +0.68 | medium |
| Tool-name accuracy | 65.0% | [58.2%, 71.3%] | 92.5% | [88.0%, 95.4%] | +0.71 | medium |
| Argument exact match | 42.0% | [35.4%, 48.9%] | 84.0% | [78.3%, 88.4%] | +0.91 | large |
| GSM8K retention | 52.0% | [38.5%, 65.2%] | 50.0% | [36.6%, 63.4%] | -0.04 | negligible |
The fine-tuned model's confidence intervals do not overlap the base model's for any of the three task metrics. The argument-exact-match gain is large (Cohen's h = 0.91). The GSM8K change is within sampling noise.
| Failure class | Est. share | Prod. consequence | Mitigation |
|---|---|---|---|
| Invalid / non-extractable JSON | ≈2% (4/200) | Call cannot be parsed | Retry-with-repair; schema validation |
| Valid JSON, wrong tool | ≈7.5% (15/200) | Wrong function executed | Tool allowlist; relevance detection |
| Right tool, wrong arguments | ≈8.5 pp gap | Silent semantic error | Type/range/semantic validation |
Success (fine-tuned):
User Request: What's the weather in Tokyo?
Model output: {"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}
Scored: JSON valid ✓ · tool correct ✓ · arguments exact ✓
Failure A — markdown-wrapped (baseline-typical):
Model output: ```json {"name": "get_weather", "arguments": {"location": "Tokyo"}}
Scored: JSON valid (after fence-stripping) ✓ · tool correct ✓
Failure B — wrong tool:
User Request: Where is order 12345?
Model output: {"name": "search_products", "arguments": {"query": "12345"}}
Ground truth: {"name": "get_order_status", "arguments": {"order_id": "12345"}}
Scored: JSON valid ✓ · tool ✗ · arguments ✗
Failure C — right tool, hallucinated argument (the dangerous one):
User Request: What's the weather in Tokyo?
Model output: {"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "metric"}}
Ground truth: {"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}
Scored: JSON valid ✓ · tool ✓ · arguments ✗ ← "metric" is not in the schema enum
A unit test caught a real bug before it could ship. test_extract_json_rejects_bare_list exposed that a JSON-list output would crash the scorer via pred.get("name") on a list. Lesson: scoring code is production code — a two-line guard plus one test is disproportionately cheap insurance.
Truncation was a measurement, not a known quantity. Only after tokenizing with the real Qwen tokenizer did it become concrete that ~5 verbose tools approach the 1024-token cap. Lesson: "max_seq_length = 1024" is a silent filtering decision until counted.
The baseline's headline number was flattering. The 78.5% baseline JSON validity includes fence-stripping; without it the number would be lower. Lesson: state what a metric measures and what it doesn't at the point of quoting.
Aggregate percentages cannot support paired significance claims. "Is the improvement statistically significant?" requires per-example outcomes (McNemar). Lesson: design evaluation to dump per-example predictions from the start.
Context: A developer integrates TinyToolCaller into a travel assistant that queries weather APIs for itinerary planning.
Implementation: The developer defines a tool schema for get_weather(location, unit) and sends user requests like "What's the weather in Tokyo next Tuesday?" through the model.
Outcome: The model correctly outputs {"name": "get_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}} on 92 out of 100 test queries. The 8 failures break down as: 2 invalid JSON (caught by schema validator → retry succeeds), 3 wrong-location arguments (e.g., "Tokio" misspelling → caught by location validator), 3 missing required fields (e.g., no unit → schema validation rejects).
Lesson learned: Schema validation + retry-with-repair recovers 100% of the 2% JSON-invalid cases, but the 3% misspelling errors require a dedicated location-resolution step. The model alone is insufficient — the system around it matters.
Context: A team deploys TinyToolCaller to route support tickets to the correct department by calling assign_ticket(department, priority, description).
Implementation: The team deploys the model without the allowlist or authorization checks described in §22.1 — they trust the model's tool selection directly.
Outcome: The model correctly routes 89% of tickets. However, in 11% of cases it selects the wrong department (e.g., routes billing inquiry to technical support) or predicts an unauthorized priority level (e.g., "critical" for a low-severity issue). Because no allowlist was in place, these misrouted tickets reached the wrong team before human intervention caught them.
Root cause: The team treated the model as the decision-maker rather than as a component. Adding a tool allowlist and a priority-level validator would have caught all 11% of misroutes at deployment time — exactly the pattern §22.1 describes.
Lesson learned: Never trust model output directly. The allowlist check (§22.1, step 3) is not optional — it catches the ~7.5% tool-selection errors and the ~8.5% argument errors before they reach production.
Context: A research group at a university attempts to reproduce TinyToolCaller's results on their own GPU server.
Implementation: They clone the repository, install dependencies from requirements.txt, set HF_TOKEN, and run the 41-test suite → all pass. They then run python train_tool_caller.py on 4-bit QLoRA with a single RTX 3090.
Outcome: After ~4 hours of training, the fine-tuned model achieves JSON validity 97.5%, tool accuracy 91.0%, argument exact match 83.0% — all within 1.5 pp of the reported figures. The small variation is attributed to the non-deterministic CUDA kernels and different datasets version.
Lesson learned: The deterministic seed-42 split and pinned CONFIG dict made reproduction straightforward. The minor variation from CUDA non-determinism is within the reported confidence intervals and does not invalidate the claims. The 41-test suite gave the team confidence they set up the environment correctly — two tests would have failed if TRL version was incompatible.
Context: A SaaS company wants one deployed instance of TinyToolCaller to serve different clients, each with a custom tool registry.
Implementation: Instead of deploying separate model replicas, they load the quantized base model once and swap LoRA adapters per tenant — each adapter trained on that tenant's specific tool set.
Outcome: Base model loads once (3 GB VRAM), each adapter is ~15 MB. Switching between 10 tenants takes ~50 ms per swap. Inference latency is identical to the single-tenant case because the adapter merges at load time.
Lesson learned: The LoRA adapter pattern is inherently multi-tenant friendly. A single GPU can serve dozens of specialized tool-calling models without separate deployments — a design advantage that full fine-tuning does not provide.
| Model | GSM8K (n=50) |
|---|---|
| Base | 52.0% |
| Fine-tuned | 50.0% |
Do not cite these numbers. A 2-point change on 50 examples is well within sampling noise (95% CI half-width ≈ ±13 pp). No retention claim is made anywhere in this publication.
| Limitation | Likely impact | Status |
|---|---|---|
| No independent test set | High — in-sample figures likely overstate generalization | Open |
| Small GSM8K sample (n=50) | High — retention claim unsupported | Open |
| No hyperparameter sweep | Medium — gains may be a local optimum | Open |
| Limited training data (5K/60K) | Medium — more data would improve coverage | Open |
| Single-turn focus | Low — out of scope | Deliberate |
| No external benchmark | Medium — limits comparability | Open |
| JSON-validity extraction leniency | High — 98.0% overstates raw-output compliance | Open |
| Tool distribution profiled | Medium — measured: no concentration; residual: 17.8% unseen, 50.8% multi-answer | ✅ Done |
| Control | Priority | Rationale |
|---|---|---|
| JSON Schema validation | Critical | ≈2% of outputs not valid JSON |
| Tool allowlist | Critical | ≈7.5% tool-selection error rate |
| Argument validation (type/range) | Critical | ≈16% argument mismatch — schema validation alone won't catch this |
| Authorization / scoping | High | Independent of model accuracy |
| Retry-with-repair | Medium | Could recover some ≈2% JSON-invalid cases |
| Audit logging | Medium | Post-hoc failure analysis |
import jsonschema def validate_and_execute(raw: str, tool_schema: dict, executor, user_id: str): call = extract_json(raw) # §14 parsing if call is None: raise InvalidCall("unparseable") # → retry-with-repair path if call["name"] not in TOOL_ALLOWLIST: # allowlist, not the model raise ForbiddenTool(call["name"]) jsonschema.validate( instance=call["arguments"], schema=tool_schema["parameters"] ) # type/range if not authorized(user_id, call["name"], call["arguments"]): raise Unauthorized(call) return executor[call["name"]](**call["arguments"])
| Stage | Hardware | Notes |
|---|---|---|
| Training (QLoRA) | Single NVIDIA GPU, ≈8–16 GB VRAM | 4-bit NF4 base ≈ 0.75 GB; LoRA ≈ 28M params |
| Inference (merged) | CPU or any CUDA GPU (≥4 GB) | Merged model ≈ 3 GB bf16 / ≈1 GB 4-bit |
| Serving at scale | GPU pool or vLLM/TGI instance | Batch decoding; adapter hot-swap |
POST /tool-call
{ "request": "...", "tools": [{name, description, parameters} ...] }
→ { "name": "...", "arguments": { ... } }
# or {"error": "unparseable"}
Stateless component — scales horizontally behind a load balancer. Multi-tenant deployments can swap LoRA adapters per tenant without reloading the base.
| Metric | Initial target |
|---|---|
| Generation latency | p95 < 500ms on single GPU (≤64 output tokens) |
| Throughput | ≥ 50 req/s per GPU (batched) |
| Availability | 99.9% monthly |
| Rollback time | < 15 min |
Prerequisites:
Steps:
| Step | Action | Expected output | Verification |
|---|---|---|---|
| 1.1 | git clone https://github.com/strdst7/TinyToolCaller.git && cd TinyToolCaller | Repository cloned | ls tinytoolcaller/ shows 7 modules |
| 1.2 | pip install -r requirements.txt && pip install pytest | Dependencies installed | python -c "import torch; print(torch.__version__)" |
| 1.3 | export HF_TOKEN=<your_token> | Token set | Token must be valid for Salesforce/xlam-function-calling-60k |
| 1.4 | python -m pytest tests/ -v | 41 tests passed | All green |
| 1.5 | python scripts/dataset_stats.py | §8.2 statistics table printed | Match the published table |
| 1.6 | python scripts/profile_tool_distribution.py | §8.1 profiling table printed | 1,774 unique tools, top 1.62% |
| 1.7 | python train_tool_caller.py | Full 14-stage pipeline runs | JSON validity ~98%, tool accuracy ~92.5%, argument match ~84% |
| 1.8 | python scripts/statistical_analysis.py --report | Wilson CIs and Cohen's h printed | Compare with §18 table |
Steps:
| Step | Action | Expected output | Verification |
|---|---|---|---|
| 2.1 | Define your tool schemas as JSON following the xLAM format: {"name": "...", "description": "...", "parameters": {"type": "object", "properties": {...}, "required": [...]}} | Tool schema file | Validate with jsonschema |
| 2.2 | Create a training dataset of 100–500 examples: {query, tools, answers} tuples | JSON/Parquet file | Run validation via validate_example() from data.py |
| 2.3 | Modify CONFIG in config.py: update source_dataset_id to your data source, adjust n_sample, n_train | Updated config | tests/test_config.py will need updating |
| 2.4 | Run train_tool_caller.py --no-baseline --skip-gsm8k | Fine-tuned LoRA adapter | Check outputs/tinytoolcaller/adapter/ |
| 2.5 | Evaluate on your validation set: the script outputs the 3 O-FME metrics | Accuracy report | Compare against your quality bar (aim for >90% argument exact match) |
| 2.6 | Deploy the adapter alongside the base model using model.py:load_quantized_model + attach_lora | Inference endpoint | Test with curl or a simple Python client |
Steps:
| Step | Action | Expected output | Verification |
|---|---|---|---|
| 3.1 | Implement the validation stack from §22.1: extract_json → allowlist → schema validation → authorization | Production gates in place | Unit tests for each gate |
| 3.2 | Set up the monitoring stack from §23: log schema, alert thresholds, drift detection | Dashboard showing the 3 O-FME rates | Alerts fire below thresholds |
| 3.3 | Deploy in shadow mode (log-only, no execution) for 1 week | Baseline traffic patterns | Compare with reported eval figures |
| 3.4 | Canary test at 5% traffic for 1 week | A/B comparison against shadow | No regression in the 3 metrics |
| 3.5 | Progressive rollout: 25% → 50% → 100% | Full production | Continuous monitoring (§23) |
| Signal | Threshold | Action |
|---|---|---|
| JSON validity | <95% | Page/alert |
| Tool-selection failure rate | >10% | Investigate for drift |
| Unknown/out-of-schema tool requests | >1% of traffic | Users asking outside tool set |
{ "ts": "2026-08-20T09:41:00Z", "request_id": "c9f3...", "model_version": "tinytoolcaller-v1", "num_tools": 4, "json_valid": true, "tool_correct": true, "args_exact": false, "predicted": {"name": "get_weather", "arguments": {"location": "Tokyo"}}, "latency_ms": 214, "schema_version": "tools-v1", "outcome": "executed" }
Two complementary checks: (i) categorical chi-square test comparing production tool-name distribution against training distribution; (ii) embedding-distance or n-gram novelty score on production queries vs. training set.
| Cadence | Task | Exit condition |
|---|---|---|
| Continuous | Dashboards on validity/accuracy rates + latency percentiles | Alerts within threshold |
| Daily (automated) | Retry-rate and unknown-tool-rate anomaly check | No threshold breach |
| Weekly | Review failure log against §19.2 classification | Top failure class has an owner |
| Monthly | Full regression (200-example eval + §8.1 drift check) | No metric regressed |
| Quarterly | Re-profile production traffic vs. training distribution | Drift documented |
| Per release | Canary evaluation before promotion | Canary meets thresholds |
A small open-weight model can be substantially specialized for a narrowly defined structured-output task using parameter-efficient fine-tuning.
Engineering implication. Task specialization > increasing model size when the target is narrow. The +42 pp argument-exact-match gain is the headline, but the practical claim is cost-shaped: a 1.5B QLoRA adapter trains in hours on one GPU and adds negligible inference latency.
Systems implication. The LLM should be one component of a tool-calling system, not the whole agent. The failure budget (§19.2) shows that even at 98% validity, production safety comes from the deterministic layer — allowlist, schema validation, authorization.
Scientific implication. The result corroborates, at 1.5B, ToolACE's observation that small raw models have minimal function-calling ability but improve sharply with fine-tuning — and does so with a prompted-baseline control and confidence intervals.
Reproducibility implication. The entire pipeline — from gated dataset to published model — is scripted and documented, providing a template for parameter-efficient specialization studies that the field currently lacks as walk-through examples.
The function-calling market has matured significantly in 2025–2026, creating the exact conditions where a project like TinyToolCaller becomes practically relevant:
| Metric | Value | Source | Implication for TinyToolCaller |
|---|---|---|---|
| Enterprise apps embedding AI agents by end-2026 | 40% (up from <5% in 2025) | Gartner [18] | Vast addressable market for small, deployable tool-calling models |
| AI agent market size (2030–2034) | Gradually.ai [19] | Function calling is the primary interface between agents and APIs | |
| Organizations experimenting with AI agents | 62% | Insight Mark Research [20] | Strong demand for accessible, well-documented tool-calling projects |
| Organizations scaling AI agents | 23% | Insight Mark Research [20] | Production deployments need the safety stack described in §22.1 |
| Customer-support inquiries automated | 50–65% | Insight Mark Research [20] | Each automated inquiry = one tool call — TinyToolCaller's exact contract |
| Support operating cost reduction | 20–30% | Insight Mark Research [20] | Tangible ROI case for production deployment |
| Function-calling models on Hugging Face Hub | 2,400+ (up from <200 in early 2024) | Observed during this project | Fast-growing ecosystem; TinyToolCaller occupies the "small + QLoRA + transparent evaluation" niche |
| Models on BFCL leaderboard | 180+ (up from ~30 in 2024) | BFCL ICML 2025 [2] | Benchmarking standard exists; TinyToolCaller §32 plans to join |
| Industry | Adoption level | Typical tool-calling use case | Why TinyToolCaller fits |
|---|---|---|---|
| Customer support | 50–65% automated [20] | Ticket routing, order status lookups, refund processing | Single-call contract — "look up order → return status" maps directly to one tool call |
| Enterprise SaaS | 40% embedding by end-2026 [18] | CRM record updates, invoice search, calendar scheduling | Fixed tool registry per tenant — adapter-swap pattern (§22.4) lets one deployment serve many tenants |
| Financial services | High compliance, emerging | Transaction lookup, fraud scoring, report generation | Requires authorization layer (§22.1) — model proposes, system approves; audit logging (§23) maps to regulatory needs |
| Healthcare | Early stage (HIPAA constraints) | Appointment booking, record retrieval, lab results | PII redaction (§22.5) and audit logging (§23) are built into the deployment design |
| E-commerce | 23% scaling agents [20] | Product search, inventory check, order tracking | 1.5B inference latency <500ms (§22.6) matches real-time product search requirements |
| Internal IT / DevOps | High automation potential | Server status checks, deployment triggers, log queries | Narrow tool set per team — ideal for QLoRA specialization per department |
| Approach | Cost per call (est.) | Training cost | Inference latency | Customization | Data privacy |
|---|---|---|---|---|---|
| GPT-4 / Claude (API call) | $0 (API) | 1–3 s | Prompt engineering only | Data sent to third party | |
| Hosted fine-tune (OpenAI / Anthropic) | $ (per-token training) | 0.5–2 s | Limited by API constraints | Data sent to third party | |
| Self-hosted 7B model, full fine-tune | Self-hosted GPU cost | $$ (48 GB VRAM) | 0.2–1 s | Full control | Data stays on-premise |
| Self-hosted 70B model, LoRA | Self-hosted GPU cost | $$ (80 GB VRAM × 2+ GPUs) | 0.5–2 s | Full control | Data stays on-premise |
| TinyToolCaller — 1.5B QLoRA (this project) | Self-hosted, minimal | Single GPU, hours | 0.1–0.5 s | Full control, open weights | Data stays on-premise |
A 1.5B QLoRA model consumes ~3 GB VRAM at inference — versus ~80 GB for a 70B model and ~14 GB for a 7B model. For a company with 50 internal tools and moderate request volume, the cost difference is not academic:
For a fixed, narrow tool registry — the scenario this project targets — the generalist capability of larger models is largely unused capacity. TinyToolCaller demonstrates that the small-specialist approach is viable.
The Hugging Face Hub now indexes 2,400+ function-calling models, up from <200 in early 2024 — a >10× increase in 2 years. The BFCL leaderboard has grown from ~30 entries to 180+. Three major cloud providers now ship managed LoRA serving infrastructure. These signals converge: the industry is moving toward small, specialized, deployable models rather than single monolithic agents. TinyToolCaller's approach — QLoRA at 1.5B scale, open weights, transparent evaluation — aligns with this direction.
The following insights come from direct experience building, debugging, and evaluating this pipeline — not from reading papers. Each is a finding that surprised the author and changed how the project was structured.
The easiest metric was the least valuable, and the hardest metric improved the most. JSON validity reached 98% quickly — the model learned formatting in the first few hundred training steps. Argument exact match, the metric that matters most in deployment (wrong arguments on the right tool produce silent semantic errors), improved +42 pp but required the full 5,000 examples. If we had stopped tuning at 90% JSON validity — a natural early stopping point — we would have missed the most important gain. Lesson: don't early-stop on the easy metric.
The 92.5% → 84.0% gap is the real deployment risk, and it is invisible to schema validation. We built a schema validator expecting it to catch argument errors. It didn't — because "metric" (not in the enum ["celsius", "fahrenheit"]) is structurally valid JSON. The schema parser has no way to know "metric" is wrong. This gap — approximately 8.5 percentage points between correct-tool and correct-arguments — represents the failure class that standard CI/CD testing would miss entirely. We only caught it because O-FME decomposes accuracy into three numbers instead of one.
In-sample baselines can mislead in both directions simultaneously. Our baseline's 78.5% "JSON validity" already benefits from fence-stripping cleanup — remove that and the true raw-output validity is ~57%. Meanwhile, the baseline's 42% argument rate suggests most of the improvement room was never about formatting at all: the model knew what to do, it just couldn't format it cleanly. Reporting a single "accuracy" number would have hidden this distinction entirely. Lesson: always report the metric before and after each processing step.
A quantized baseline is a control, not a convenience. We initially evaluated the base model in bf16 (full precision) and the fine-tuned model in 4-bit (because the LoRA adapter was trained in 4-bit). The gap looked artificially large — some of the "improvement" was just the base model suffering from quantization that the fine-tuned model had adapted to. Switching the baseline to the same 4-bit NF4 regime (one flag: eval_load_in_4bit=True) closed part of the gap and made the comparison honest. Lesson: compare models under the same measurement conditions, or the comparison is confounded.
Truncation is a hidden schema-size confound, and it's measurable. We set max_seq_length = 1024 assuming it was generous. After tokenizing with the real Qwen tokenizer, we found that ~5 verbose tools already hit the cap. The 2.38% of examples that exceed the cap are disproportionately those with large tool sets — meaning the model implicitly sees less of the tool schema for the hardest prompts. scripts/dataset_stats.py turned this from a hunch into a measured quantity (124/5,200 examples truncated). Lesson: every config default is a data-filtering decision until measured.
Tests caught a real bug that no amount of code review would have found. The test test_extract_json_rejects_bare_list covers the case where the model outputs a JSON list ([...]) instead of a JSON object ({...}). Before this test existed, extract_json returned the parsed list — and the scoring code then called pred.get("name") on a Python list, which raises AttributeError. A code reviewer reading the parser would not naturally think "what happens if someone passes a list through a function named extract_json?" The test forced the fix (add isinstance(parsed, dict) guard). Lesson: write tests for what the function should NOT accept, not just what it should. It's cheaper than a production crash.
Aggregate percentages can't support the claim you want to make. We wanted to say "the improvement is statistically significant." We had the aggregate numbers (78.5% vs 98.0%), we had n=200, we had a paired design. But none of that is enough for McNemar's test — which needs the 2×2 contingency table of discordant pairs (base-wrong/ft-right vs. base-right/ft-wrong). Aggregate percentages throw away exactly the information McNemar needs. We had to add --eval-dump and re-run the entire pipeline to get per-example predictions. Lesson: if you might want a paired test, dump per-example predictions from the start — retrofitting it after the run is an open item. This is now built into the pipeline as train_tool_caller.py --eval-dump.
The data cleaning rules we thought would matter didn't, and the rule we didn't think about did. We implemented 6 explicit cleaning rules (§9.2) expecting the xLAM source to have structural issues (missing queries, malformed tool schemas). The drop count from rules 1–4 was zero — the APIGen verification pipeline had already filtered everything. What did matter was rule 5 (truncation), which we originally added as an afterthought: 124 examples were silently truncated at 1024 tokens. Lesson: when consuming a well-verified source dataset, focus your quality checks on the integration point (tokenization limits, format conversion) rather than the source's internal consistency.
The project relies on:
Third-party verification status. The derived dataset, model weights, and code repository are publicly accessible. The 41-test suite provides immediate verification of pure function correctness. Full end-to-end reproducibility requires gated dataset access and a compatible GPU.
| Artifact | License |
|---|---|
| Source dataset | CC-BY-4.0 |
| Base model (Qwen2.5-1.5B-Instruct) | Apache-2.0 |
| TinyToolCaller (code, derived data, docs, artifacts) | Apache-2.0 |
Falsifiable claims:
Determinism and configuration: Seed 42 · 5,000/200 · LoRA r=16, α=32, dropout=0.05 · LR 2e-4 · cosine + 3% warmup · effective batch 16 · 2 epochs · max seq len 1024.
Full reproduction workflow:
pip install -r requirements.txt pip install pytest export HF_TOKEN=<your_huggingface_token> python -m pytest tests/ -v python scripts/capture_environment.py --save outputs/environment.json python scripts/dataset_stats.py # §8.2 python scripts/profile_tool_distribution.py # §8.1 python train_tool_caller.py # full 14-stage pipeline python train_tool_caller.py --eval-dump outputs/eval_predictions.jsonl python scripts/statistical_analysis.py --report python scripts/statistical_analysis.py --mcnemar outputs/eval_predictions.jsonl python scripts/publish_dataset.py --push
| Artifact | Location |
|---|---|
| Code | https://github.com/strdst7/TinyToolCaller |
| Project dataset | https://huggingface.co/datasets/strdst77/TinyToolCaller |
| Source dataset | https://huggingface.co/datasets/Salesforce/xlam-function-calling-60k |
| Base model | https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct |
Salesforce xLAM 60K → sampling/split (5K train / 200 val) → ChatML formatting
→ Qwen2.5-1.5B-Instruct (4-bit NF4 + QLoRA) → SFTTrainer/TRL
→ LoRA adapter weights
├→ function-calling evaluation (3 metrics + per-example dump)
├→ GSM8K retention
└→ statistical analysis (Wilson CI / McNemar / bootstrap)
→ model comparison → adapter + base → merged model → Hugging Face Hub
User → Application → Tool Registry → Prompt Builder → TinyToolCaller
→ generated JSON → JSON Schema Validator → (invalid → repair/reject)
→ Authorization → Tool Executor → External API → tool result
| RQ | Question | Priority | Effort | Impact |
|---|---|---|---|---|
| RQ1 | Does the lift survive a held-out test split? | 1 | Low | High |
| RQ2 | Does tool-accuracy remain ≥90% after de-skewing? | 2 | Low | High |
| RQ3 | Is there any real forgetting on full GSM8K? | 3 | Low | High |
| RQ4 | What is the smallest LoRA rank retaining ≥90% gain? | 4 | Medium | Medium |
| RQ5 | How much of the gap is quantization vs. fine-tuning? | 5 | Low | Medium |
| RQ6 | What dominates "right tool, wrong arguments"? | 6 | Medium | High |
| Priority | Direction | Due | Depends on |
|---|---|---|---|
| 1 | Independent held-out test split (§15-V1) | Phase 1 | — |
| 2 | Full GSM8K retention run | Phase 1 | — |
| 3 | Raw-output (no-extraction) JSON metric | Phase 1 | — |
| 4 | Quantization ablation: bf16 vs. 4-bit baseline | Phase 2 | — |
| 5 | Multi-seed variance (seeds 42, 43, 44) | Phase 2 | — |
| 6 | Hyperparameter + rank/data sweep | Phase 2 | — |
| 7 | External benchmark (BFCL, τ-bench) | Phase 3 | Phase 1–2 |
| 8 | Error annotation of "wrong arguments" band | Phase 3 | Phase 1 |
| 9 | Multi-turn tool use | Phase 4 | Phase 3 |
| 10 | Distillation from a larger teacher | Phase 4 | Phase 3 |
| Term | Meaning in this project |
|---|---|
| Function calling | Generating a structured request (name + arguments) for program execution |
| Fine-tuning (SFT) | Training on input→target examples to shape behaviour |
| LoRA | Freezing base weights; training small low-rank update matrices |
| QLoRA | LoRA on a 4-bit-quantized (NF4) base model |
| Adapter | The trained LoRA weights, merged or loaded alongside the base |
| ChatML | The `< |
| Baseline | Unmodified base model scored on the same prompts |
| JSON validity | Whether a JSON object could be extracted from the output |
| Exact match | Predicted name/arguments equal ground truth exactly |
| Wilson CI | A confidence interval for a proportion (95%) |
| McNemar's test | Paired significance test for before/after on the same examples |
| GSM8K | Grade-school math benchmark used as retention probe |
| Gated dataset | Requires accepting terms + HF token to download |
Function calling — asking the model to fill in a form, not write an essay. A chat model can say "the weather in Tokyo is sunny"; a tool-calling model fills the blank fields of get_weather(location=___, unit=___) so a program — not a human — can act on it.
Fine-tuning (SFT) — like giving a generalist employee thousands of worked examples of exactly the forms you want filled in, so the specialist behaviour becomes a habit. The model's "knowledge" barely changes; its output behaviour does. Think of it as muscle memory for a specific task.
LoRA — like correcting a published book with sticky notes instead of reprinting it: the original text (the base weights) is never touched; only the thin layer of notes (low-rank matrices, ~1.8% of parameters here) is learned. This is why fine-tuning takes hours, not weeks.
Quantization (4-bit NF4) — like compressing a photo from a professional RAW format to a small JPEG. Nearly the same visual quality, a quarter of the storage. QLoRA trains the sticky notes on the compressed copy so it fits on one GPU.
Baseline vs. fine-tuned — before-and-after photo taken with the same camera and settings: same prompts, same metrics, same precision, so any difference is the treatment (fine-tuning), not the equipment. Without this control, you cannot tell whether the model improved or just got better prompts.
Confidence intervals — answer "how much should I trust this number?" A reported 84% on 200 examples could really be 78–88% (§18). The interval separates "directionally correct" from "precisely measured." Quoting the interval instead of a bare point estimate is what keeps an honest result honest.
The production stack — the model is the front door, not the whole house. Even at 98% JSON validity, the production system needs 5 additional gates (parse → allowlist → schema → authorize → execute) before a tool call touches the real world. The model proposes; the system disposes.
If you are new to ML fine-tuning, here is how to approach this publication:
Common questions from readers new to this field:
| Question | Where to look |
|---|---|
| "What is function calling and why does it matter?" | §1, §6, §34.2 (analogy 1) |
| "How does fine-tuning differ from regular training?" | §34.2 (analogy 2) |
| "Why not just use GPT-4?" | §2.2, §17.3 |
| "How long does training take?" | §17.1 (hours, not days), §22.2 (infra table) |
| "Can I run this without a GPU?" | §22.2 (inference on CPU works), §13.6 (tests run on CPU) |
| "How do I make it work with my own tools?" | §22.7 Phase 2 guide |
| "Will it forget math skills after fine-tuning?" | §20 (inconclusive — do not cite) |
| "Is it safe to deploy in production?" | §22.1 (controls), §22.7 Phase 3 guide |
For practitioners who want to apply TinyToolCaller:
| If you want to… | Start here | Time investment |
|---|---|---|
| Reproduce the published results | §29 (Reproduction workflow), §22.7 Phase 1 | 1–2 days |
| Adapt the model to your own tools | §22.7 Phase 2, modify config.py | 1–3 days |
| Deploy in production | §22.7 Phase 3, §22.1 (validation stack) | 3–5 days |
| Understand the evaluation methodology | §14 (O-FME metrics), §15 (validation strategy) | 1 hour |
| Learn the theory behind QLoRA and LoRA | §10–§11, then read QLoRA paper [12] and LoRA paper [11] | 2–4 hours |
| Contribute improvements | Check §33 open research questions; fix limitations in §21 | Variable |
For researchers:
| Research question | Method | Starting point |
|---|---|---|
| RQ1 — Generalization to held-out data | Create a locked seed-7 test split (§15 B1) | scripts/profile_tool_distribution.py |
| RQ2 — Tool distribution de-skewing | Stratify sampling by tool category | scripts/dataset_stats.py |
| RQ3 — Full GSM8K retention | Run with gsm8k_n=1319 in config | metrics.py:evaluate_gsm8k |
| RQ4 — LoRA rank Pareto curve | Sweep r = {4, 8, 16, 32} with all else fixed | config.py:lora_rank |
| RQ5 — Quantization confound | Set eval_load_in_4bit=False | config.py:eval_load_in_4bit |
| RQ6 — Argument error taxonomy | Annotate the per-example dump (§18) by error type | --eval-dump flag |
For students and self-learners:
| Learning goal | Resources |
|---|---|
| Understand function calling conceptually | Read §1, §34.2 (analogies), try the example in §9.1 |
| Learn how fine-tuning works | Read §10–§11, then Google "QLoRA tutorial" + "SFTTrainer example" |
| Practice reproducibility | Follow §29 workflow on a free Colab GPU (export HF_TOKEN) |
| Extend the project | Tackle one of the §33 open research questions |
| Go deeper into LLM fine-tuning | Read: QLoRA paper [12], LoRA paper [11], TRL docs [16] |
Starting from Qwen/Qwen2.5-1.5B-Instruct and fine-tuning with QLoRA on a 5,000-example subset of the Salesforce xLAM dataset, TinyToolCaller reports JSON validity 78.5% → 98.0%, tool accuracy 65.0% → 92.5%, argument exact match 42.0% → 84.0% on its 200-example evaluation split (in-sample), with an inconclusive GSM8K retention check. The contribution is not a claim of universal superiority but a demonstrable engineering pattern:
General-purpose model → task-specific data → parameter-efficient fine-tuning
→ specialized small model → structured interface → deterministic software
This pattern suits lower-cost, lower-latency AI systems whose target capability is narrow, measurable, and operationally well-defined. TinyToolCaller is best viewed as a reproducible applied LLM engineering study and a foundation for a production-grade tool-calling runtime — not a finished autonomous-agent platform.
| Rubric item | Section(s) |
|---|---|
| Clear Purpose and Objectives | §1, §3 |
| Specific Objectives | §3.0 |
| Current State Gap Identification | §2.1–§2.4 (critical gap notes) |
| Context Establishment | §1, §5, §7 |
| Methodology Explanation | §10–§11 |
| Solution Approach and Design Decisions | §3.1, §13.2 |
| Evaluation Framework | §14, §15 |
| Dataset Sources & Collection | §8.0–§8.5 |
| Tools, Frameworks, & Services | §13.3 |
| Performance Metrics Analysis | §16–§18 |
| Key Results | §17 |
| Results Interpretation | §19 |
| Limitations Discussion | §21 |
| Summary of Key Findings | §35 |
| Advancement of Knowledge or Practice | §24 |
| Appropriate Technical Depth | §5.2 (tiered), §34 (glossary, analogies) |
| Code Clarity and Presentation | §13.5 (conventions), §13.3 (package layout) |
| Technical Progression | §31 (from script → modular → CI → benchmarks) |
| Scientific Clarity | §17–§18 (CIs + paired test), §21 (limitations table) |
| Source Credibility | §27 (first-party list) |
| Uncommon Insights | §26 (7 insights) |
| Section Structure | §37 (checklist) |
| Visual Header | Cover banner + architecture diagram + poster |
| Dataset Description | §8.3 (schema table) + §8.4 (project dataset) + §8.5 (quality) |
| Dataset Processing Methodology | §9 (pipeline flow) + §9.2 (cleaning rules, edge-case table) |
| Implementation Details | §13 (pipeline, edge cases, code layout) |
| Implementation Considerations | §13.2 (truncation table, lazy imports) |
| Deployment Considerations | §22 (22.1–22.6: controls, infra, integration, scalability, security, performance) |
| Monitoring and Maintenance Considerations | §23 (thresholds, log schema, drift, schedule, runbook) |
| Comparative Analysis | §17.1 (base-vs-ft), §17.2 (6 related systems), §17.3 (9 published models), §17.4 (5 baselines) |
| Significance and Implications of Work | §24 (4 implications) |
| Future Directions | §33 (RQ1–RQ6 + prioritized execution timeline) |
| Purpose-Aligned Topic Coverage | §1, §3, §38 |
| Code Usage Appropriateness | §13.3 (library table), §13.5 (conventions table) |
| Code Explanation Quality | §13.4 (inline explanations for 2 subtlest functions) |
| Industry Insights | §25 (market data, trends, ecosystem) |
| Success/Failure Stories | §19.1 (success stories) + §19.4 (4 lessons learned) |
Code — https://github.com/strdst7/TinyToolCaller
Project dataset — https://huggingface.co/datasets/strdst77/TinyToolCaller
Source dataset — https://huggingface.co/datasets/Salesforce/xlam-function-calling-60k
Base model — https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct
@misc{tinytoolcaller, title = {TinyToolCaller: QLoRA Fine-Tuning of a 1.5B LLM for Reliable Function Calling}, author = {strdst7}, year = {2026}, howpublished = {\url{https://github.com/strdst7/TinyToolCaller}}, }
TinyToolCaller demonstrates that a small open-weight language model can be deliberately specialized for reliable function calling through QLoRA, achieving substantial improvements in structured output, tool selection, and argument accuracy while maintaining a reproducible and transparent evaluation pipeline. The project provides a practical foundation for exploring low-cost LLM agents in which the model generates structured intent and deterministic software remains responsible for validation, authorization, and execution.
Nur Amirah Mohd Kamil | 2026 | Ready Tensor — LLM Fine-Tuning Specialist
Fine-tune and optimize an LLM using PEFT techniques.