--- license: mit base_model: Qwen/Qwen3.5-4B library_name: peft pipeline_tag: text-generation language: - id - jv - su datasets: - fairleap-ai/fairleap-driver-chat-sft-43k tags: - base_model:adapter:Qwen/Qwen3.5-4B - lora - qlora - sft - transformers - trl - unsloth - conversational - tool-use - indonesian - gig-economy - ride-hailing - fairleap ---

Fairleap v1 CLM Qwen3.5-4B Adapter

## 📘 Model Overview A QLoRA adapter that turns [`Qwen/Qwen3.5-4B`](https://huggingface.co/Qwen/Qwen3.5-4B) into a driver-welfare assistant for Gojek/GOTO partners in Indonesia, built for the [Fairleap AI](https://github.com/Fairleap-AI) project — a platform addressing income uncertainty and wellbeing for ride-hailing drivers. It is trained for three behaviours: **answering from context it was handed** rather than asking for data, **calling a forecasting tool** when a question needs arithmetic a language model cannot do, and **declining** what falls outside the product. Training data is [`fairleap-ai/fairleap-driver-chat-sft-43k`](https://huggingface.co/datasets/fairleap-ai/fairleap-driver-chat-sft-43k). > [!IMPORTANT] > Trained entirely on **synthetic** conversations that no human reviewed, and **no held-out > benchmark has been run** — the evaluation below is a 10-conversation probe, not a score. The > model also **over-calls its forecasting tool**, reaching for an earnings prediction on > questions about fatigue and traffic. Read > [Limitations & Biases](#%EF%B8%8F-limitations--biases) before putting it in front of anyone. ## 🚀 Usage This repository holds a **LoRA adapter only** — no base weights. `load_model.py` handles the three things that otherwise look like broken weights: ```python from load_model import load, chat, build_system_prompt, PREDICT_EARNINGS_TOOL model, tokenizer = load() # base + adapter, 4-bit system = build_system_prompt( today="2026-08-24", city="Bekasi", vehicle="motor", risk="sedang", wellness_score=62, period="2026-08-18 s/d 2026-08-24", totals={"Total penghasilan": "Rp1.482.000", "Total order": "88", "Hari kerja": "6 dari 7 hari", "Rata-rata per hari kerja": "Rp247.000"}, ) print(chat(model, tokenizer, [ {"role": "system", "content": system}, {"role": "user", "content": "berapa penghasilan saya minggu ini?"}, ])) ``` Offer the tool only when a forecast is plausibly needed — see Limitations: ```python reply = chat(model, tokenizer, messages, tools=[PREDICT_EARNINGS_TOOL]) from load_model import parse_tool_call parse_tool_call(reply) # ('predict_earnings', {'start': '2026-08-25', 'end': '2026-08-27', 'wellness_score': 62}) ``` ### Three traps, handled for you 1. **The base is a vision-language model.** `Qwen3_5ForConditionalGeneration` — so `from_pretrained` returns a `Qwen3VLProcessor`, not a tokenizer, and its `__call__` reads the first positional argument as an *image source*: `processor("halo")` raises `Incorrect image source`. `get_tokenizer()` extracts the inner text tokenizer. 2. **Adapter-only directory.** `AutoModelForCausalLM.from_pretrained(".")` fails and PEFT then retries the local path as a Hub repo id, reporting `HFValidationError: Repo id must be in the form 'repo_name'…`, which reads as a path bug. 3. **Tool calls come back as Qwen's XML block**, not the JSON the corpus stored: `…`. `parse_tool_call()` recovers it. **`fairleap-api` must parse this shape.** ### The system prompt carries the driver The service this was trained for is stateless and identity-blind: every fact about a driver arrives in the request. Training prompts ran ~4,400 characters — persona, style, prohibitions, then city, vehicle, BPJS status, risk tolerance, a 7-day summary and up to 14 daily rows. Give it less and it has less to be correct about; give it nothing and it has nothing to read. ## 🗂️ Model Details | | | |---|---| | Base model | `Qwen/Qwen3.5-4B` (`Qwen3_5ForConditionalGeneration`, multimodal) | | Method | QLoRA, 4-bit NF4, r=32, α=32, dropout 0.0, bias none | | Trainable params | 42.5M across 256 tensors | | Target modules | `q_proj` `k_proj` `v_proj` `o_proj` `gate_proj` `up_proj` `down_proj` | | Adapter size | 170 MB (`adapter_model.safetensors`) | | Context | 4,096 tokens | | License | MIT | **The vision tower is untouched.** Every adapted module sits under `model.language_model.*`. The projection names are shared across both stacks, so an unscoped target list would silently adapt a tower that never sees an image — the training script asserts the scoping rather than assuming it, and the shipped safetensors was re-checked: 256 tensors, none outside the text decoder. ## 📊 Training | | | |---|---| | Data | 12,000 stratified conversations from `fairleap-driver-chat-sft-43k` train | | Preprocessing | canned follow-up turns truncated (`training/prepare_data.py`) | | Epochs / steps | 2 / 1,500 | | Batch | 8 × 2 accumulation = effective 16, length-grouped | | LR | 2e-4 cosine, warmup ratio 0.03, `adamw_8bit`, weight decay 0.01 | | Masking | `train_on_responses_only` — ~11% of tokens enter the loss | | Seed | 20260819 | | Hardware | 1 × A100-SXM4-40GB, 270 minutes | The 12,000 sample preserves the corpus mix to within **0.04 percentage points** on scenario, language and source, and keeps all 17 scenarios and all 5 language registers. Loss fell monotonically and **was still falling at the end**: | epoch | 0.4 | 0.8 | 1.2 | 1.6 | 2.0 | |---|---|---|---|---|---| | eval_loss | 1.089 | 1.032 | 1.004 | 0.9843 | **0.979** | Final train loss 1.029, so the train/eval gap stayed ≈0.05 — no overfitting, and headroom for more epochs or more data rather than less. `training/` reproduces the run end to end. ## 📈 Evaluation > **There is no held-out benchmark.** A full scoring pass over the 427-conversation test split > was started and stopped on cost grounds. What follows is a **10-conversation probe** plus six > qualitative prompts. Treat every number here as indicative, not measured. `eval_model.py` ships in this repo and scores the failure modes that matter for this product — invented Rupiah figures, out-of-scope tools, malformed or unsolicited tool calls, language drift, refusal erosion. To run it properly: ```sh python eval_model.py --backend unsloth --model . --test fairleap_test.jsonl ``` **10-conversation probe**, tool offered on every turn: | | | |---|---| | Language drift (CJK leakage) | 0 / 10 | | Forbidden / demoted tool mentions | 0 / 10 | | Tool calls emitted | 8 | | Malformed tool calls | **0 / 8** | | Tool-call overruns | **0 / 8** | | Grounding failures | 0 / 4 audited | | Refusal misses | 0 / 1 audited | Argument formation is the strong result: 8 calls, zero malformed, and zero overruns — generation stops at the call instead of inventing the forecast it was about to request. **Six qualitative prompts** (verbatim output in the session that produced this adapter): grounded recall quoted every figure exactly from the stuffed context and invented none; wellness advice reproduced the rest/hydration/clinic guidance; financial advice respected the stated risk tolerance; the out-of-scope prompt declined to draft a divorce petition and redirected. ## ⚠️ Limitations & Biases **It over-calls the forecasting tool.** In the 10-conversation probe it called `predict_earnings` on **8 of 10** conversations, including `wellness` (*"badan saya capek terus"*), `traffic_route` and `data_absent` — where a forecast is simply the wrong response. It declined to call only on `clarification` and `out_of_scope`. Root cause is a distribution mismatch: under 10% of training conversations carried a tool, so "tool offered, not needed" is under-represented. **Mitigation: offer `PREDICT_EARNINGS_TOOL` only on turns where a forecast is plausible**, and treat a call on a wellness or routing question as a bug in the caller, not a signal from the driver. Quantifying this properly needs the full evaluation. **No held-out score.** Everything above is 10 conversations and 6 prompts. Rates below ~10% cannot be distinguished from zero at that sample size. **Javanese drift.** Asked a question in Javanese, it answered in casual Indonesian — correct content, wrong register — despite `jv` being 10.2% of the corpus. Score `jv` and `su` separately; a corpus-level average hides this. **It embellishes beyond context.** Asked for a weekly summary it rendered 2026-08-21 as *Minggu* (Sunday) when that date is a Friday — a weekday that was never in the prompt. Figures were grounded; the decoration around them was not. **Date-range interpretation is loose.** Asked about *minggu depan* ("next week") it requested a three-day window, consistently across probes. Validate `start`/`end` before executing the call. **Not converged.** Trained on 12,000 of 41,461 available conversations, and eval loss was still improving at 2 epochs. This is a v1, not a finished model. **Inherits every dataset limitation** — fully synthetic, unreviewed advice, teacher-model errors about Indonesian financial products, simulated tool results. See the [dataset card](https://huggingface.co/datasets/fairleap-ai/fairleap-driver-chat-sft-43k#%EF%B8%8F-limitations--biases). **Not professional advice.** It discusses debt, insurance selection and investment for a financially vulnerable population, and none of it was expert-reviewed. Not suitable for estimating real driver income, informing platform or labour policy, or any claim about actual gig-economy conditions in Indonesia. **Vision is untrained.** The base accepts images; this adapter never saw one. Image input is out of scope and unvalidated. ## 🛠️ Tech Stacks - **peft**: The Hugging Face library implementing LoRA and other parameter-efficient finetuning methods. - **unsloth**: A finetuning library giving roughly 2x faster training and much lower VRAM use than stock peft+trl. - **trl**: The Hugging Face supervised-finetuning and RL library that ran the training loop. - **transformers**: The Hugging Face library providing the base model, processor and chat template. - **bitsandbytes**: The 4-bit NF4 quantisation backend that lets a 4B model train on one 40 GB card. - **torch**: The deep learning framework everything above runs on. ## 📚 Citation ```bibtex @misc{fairleap_v1_clm_qwen35_4b_adapter, title = {Fairleap v1 CLM Qwen3.5-4B Adapter}, author = {Fairleap AI}, year = {2026}, publisher = {Hugging Face}, howpublished = {\url{https://huggingface.co/fairleap-ai/fairleap-v1-clm-qwen3.5-4b-adapter}} } ``` ## 📝 License This adapter is licensed under the MIT License. The base model `Qwen/Qwen3.5-4B` carries its own license (Apache 2.0); using this adapter means loading that model too.