Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 9 min read

A 4B model on my computer drafts freelance quotes. I tested it on 8 real client briefs.

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle What I Built A freelancer gets a vague client brief, guesses the hours, and finds out three weeks later the guess was wr

A 4B model on my computer drafts freelance quotes. I tested it on 8 real client briefs.

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

A freelancer gets a vague client brief, guesses the hours, and finds out three weeks later the guess was wrong. briefquote makes that guess less of a guess, and the brief never leaves your computer.

It splits the work between two open models. Gemma (gemma3:4b, running locally through Ollama) reads the messy prose and extracts a structured scope: project type, deliverables, integrations, a 1 to 5 complexity rating, plus the questions to ask the client and the risks to price in. Six of those fields become one table row. TabPFN, a pretrained model for small tables, learns from your own past jobs and turns that row into an hours range, a price and an on-time probability. You get a Markdown quote draft to review before it goes out.

On 8 real client briefs it returned valid output every time, and 3 were usable as a first draft without major edits. Most of this post is about that gap.

Who would pay: freelancers who would rather not paste a client's brief into a hosted chatbot. Nobody has paid yet.

One disclosure first: the demo's job history is 30 synthetic jobs, so its hours and prices show the pipeline working, not real-world accuracy. Every Gemma output in this post comes from a real run.

Demo

briefquote web UI: a real gemma3:4b run on the SaaS sample brief

A real gemma3:4b run on a CPU-only machine. The wait (about a minute) is shortened; the elapsed counter is genuine. Why the on-time figure shows 5% is explained in "The estimator" below.

Try it in your browser, nothing to install: briefquote replay. Pick one of three sample briefs to see real output. Nothing on the page calls a model; only the hourly-rate arithmetic recalculates. Gemma's output varies between runs: the replay shows 66.8 h for the SaaS brief, the GIF run 79.1 h, and another run 59.2 h.

The real tool is a local web UI (briefquote serve) bound to 127.0.0.1 with no external requests. Paste a brief, set your rate, and copy the Markdown quote.

Code

GitHub logo Moin105 / briefquote

Local scope-and-quote tool for freelancers: Gemma via Ollama extracts a scope from a client brief, TabPFN estimates hours and price. DEV Hacktoberfest Open-Source AI Challenge Week 2.

briefquote

tests

Live replay of real runs, nothing to install: https://briefquote-replay.vercel.app

Quote from a client brief without the brief leaving your laptop. Paste a client brief, and briefquote uses an open-weight model (Gemma, running locally through Ollama) to extract a structured scope as JSON: deliverables, tech stack, complexity, integrations, unknowns to ask the client, and risk flags.

Why open-weight

  • Privacy: client briefs often contain confidential business details. Nothing is sent to a third-party API.
  • Zero per-call cost: quote as many briefs as you like.
  • Works offline: once the model is pulled, no internet is needed.

Status

  • Day 1: brief-to-scope extraction. Done.
  • Day 2: TabPFN-based hour and price estimator trained on your past jobs (data/past_jobs.csv is a synthetic example dataset, not real client work; see data/README.md), plus a client-ready Markdown quote. Done.
  • Day 3: local web UI (briefquote serve). Done.
  • TabPFN path: verified on real model weights on…

MIT licensed. 76 tests run without Ollama, and CI runs them on Windows and Linux with Python 3.11 to 3.13. The one real-weights TabPFN test needs a Prior Labs key, so CI skips it; I ran it locally.

Started October 14, 2026, inside the challenge window. Claude Code agents wrote the implementation; I directed and verified it, including running the real models.

How I Built It

The pipeline extracts a scope from the brief, estimates hours and price, and renders the quote. Extraction is where a 4B model needs the most help.

The JSON schema goes to Ollama in the format field, constraining decoding to the right shape. Pydantic validates the result, and on failure the error goes back to the model once.

schema = ScopeExtraction.json_schema_for_llm()
user = base_prompt = build_user_prompt(brief)
for _ in range(max_attempts):  # 2
    raw = client.complete_json(SYSTEM_PROMPT, user, schema)  # sent as "format": schema
    try:
        return ScopeExtraction.model_validate(json.loads(strip_fences(raw)))
    except (json.JSONDecodeError, ValidationError) as exc:
        user = f"{base_prompt}\n\nYour previous output was invalid: {exc}\nReturn only valid JSON."

Here is a real run, trimmed from examples/outputs/saas-dashboard.json:

{
  "project_type": "web",
  "complexity": 3,
  "integrations_count": 2,
  "has_design_provided": true,
  "client_timeline_days": 35,
  "budget_mentioned_usd": 6000.0,
  "risk_flags": ["Client requesting additional bug fixes outside the initial scope - scope creep", "..."]
}

The brief said "5 weeks" and "$6,000"; the model turned those into 35 and 6000.0 only after I changed the prompt, whose examples use those exact conversions. Tuning on the same three briefs I tested on proves little, so the real test below uses eight briefs the prompt never saw.

The first real run broke

The agent-written tests passed against a fake client. Then I ran the real model on three demo briefs on a CPU-only Windows machine (AMD Ryzen 5 5600U, 15 GB RAM) at about 7 tokens per second.

The original prompt failed on 2 of 3.

  • SaaS dashboard: both attempts returned complexity: 8, which my validator rejects above 5; the schema Ollama saw carried no such bound. It also left out the timeline and budget and listed field names as unknowns.
  • Shopify descriptions: Ollama returned HTTP 500, "prediction aborted, token repeat limit reached". The model looped and the server killed it, reproducibly.
  • Dental bot (valid JSON, wrong content): a WhatsApp bot labelled mobile, complexity 1, for a job touching patient data and a third-party system.

I made two rounds of prompt changes, with no structural code changes. The rules that mattered: ai_automation for bots and LLM tools, mobile only for native apps; complexity an integer from 1 to 5; lists capped at 8 short, non-repeating items (the abort did not recur in my run, though one run is not proof).

Brief Before After
saas-dashboard invalid, complexity 8 valid, 35 days and 6000.0 captured
shopify-descriptions HTTP 500 valid
dental-whatsapp-bot mobile, complexity 1 ai_automation, complexity 3

After the second round, all three returned valid JSON, in 55 to 57 seconds each.

What I fixed, and what is still wrong

Three code fixes followed: an aborted generation retries once with a higher repeat penalty (tested only with mocked responses); output is forced to UTF-8, since redirected Windows output used a legacy code page and produced invalid JSON; and the 1 to 5 bound went into the JSON schema itself, after which the next real SaaS run came back valid with complexity 3.

Valid is not good. Complexity was 3 for all three briefs, so the rating barely discriminates. Some unknowns are padding ("What is the client's experience with CSV files?").

Then I tried real briefs

The three samples were AI-written and I tuned the prompt on them, so I wanted real client briefs the prompt had never seen. I took 8 public job posts from a freelance marketplace, stripped names, emails, phone numbers and URLs, and did not republish the texts.

Measure Result
Briefs run 8
Valid output 8 of 8
Usable as a first draft without major edits 3 of 8
Median time per brief (CPU) 63.5 s

Three more needed real edits to type, unknowns and risks. Two dropped half the scope; both listed many separate deliverables. That is the expensive failure for a quoting tool: deliverable count feeds the estimator, so missing scope becomes an underquote. The failures were omission, label mistakes and filler, not fabrication: zero clear invented deliverables or technologies, one mild. Data work was labelled ai_automation three times. Quoted client sentences showed up as risk flags in 4 of 8 briefs, and one fill-in-the-blank template line was read as a real 7-day timeline. Estimates clustered between $1,450 and $3,900 regardless of scope, which fits a history of only 30 synthetic jobs.

The check also found a bug in my setup. Ollama's default context window silently cut a 3,811-word stress input (one real brief repeated five times) to 2,051 tokens, with no warning. The client now raises num_ctx automatically for long briefs, up to 32,768 tokens, and errors clearly beyond that. After the fix, a synthetic 3,728-word input was read in full: 5,472 prompt tokens. Briefs under 700 words keep Ollama's defaults.

My verdict on a 4B model: a checklist and clarifying-question generator that works best on short, clear briefs. It drops scope on long, structured ones and still needs a human reading the brief. Full report: https://github.com/Moin105/briefquote/blob/main/docs/real-brief-check.md.

The estimator

TabPFN is a pretrained transformer for small tabular datasets, so it can learn from 20 or 30 past jobs instead of thousands. briefquote fits its regressor for hours and price, using the quantile output (10th, 50th and 90th percentile) as the range, and its classifier for the on-time chance. It runs on CPU with real weights (tabpfn-v3.5, package tabpfn 9.2.0). Without TabPFN, a k=5 nearest-neighbour baseline runs, and the quote says which.

On the hand-written sample scope, the baseline said 95 h (48.8 to 112 h) and TabPFN said 91.9 h (80.1 to 102.4 h). A tighter range is not a more accurate one: on synthetic history this shows only that the plumbing works.

The on-time figure taught me the most. In my synthetic history, all 12 jobs with unclear requirements ran late or were cancelled, against 17 of 18 clear ones delivered on time, and TabPFN learned exactly that rule: flipping only has_clear_requirements moves the raw probability from 97.6% to 4.0%. Gemma marked all three sample briefs as unclear, so every demo quote shows 5%, the floor of the 5 to 95 percent clamp. TabPFN is faithful to the history it is given, which is the argument for fitting it on your own.

Real weights also caught a bug: the recommended price ($4,250) sat above its own ceiling ($4,050), because floor and ceiling ignored the rate-based half of the blend. All three now use the same blend, so floor <= recommended <= ceiling by construction ($3,700, $4,250, $4,700 at $40/h).

Why Does Open Innovation Matter?

Two kinds of private data stay on the computer. A client brief is confidential, and pasting it into a hosted service decides on the client's behalf. A freelancer's job history is sensitive too: rates, clients, and the jobs that ran late. Here both stay local: Gemma reads the brief and TabPFN fits the history on the same machine.

Owning the runtime cuts both ways. Running the models myself put every knob in my hands: the JSON schema for constrained decoding, the context window, the retry repeat penalty, and the model (--model swaps it). It also meant I inherited a default that silently cut a long input to 2,051 tokens, where hosted APIs typically return an error. Open means the defaults are yours, including the ones you did not know about.

Open weights are not always open for business. TabPFN's weights are licensed for non-commercial use only; a commercial license comes from Prior Labs. So briefquote keeps a nearest-neighbour baseline with no ML dependencies (--no-tabpfn) that anyone selling quotes can use with no license question. briefquote's own code is MIT.

What it cost: speed and judgement. About a minute per brief on CPU, and a 4B model that drops scope on long briefs. In return there is no per-call fee, and it works offline once the models are downloaded (TabPFN's weights need a one-time free Prior Labs sign-in and license). I did not run a hosted model on the same briefs, so I cannot say how much quality the privacy cost.

Prize Categories

  • Gemma (gemma3:4b, local through Ollama, CPU only): the reading half. It turns an unstructured brief into the structured scope and the six-field row the estimator needs, with schema-constrained JSON, Pydantic validation and one corrective retry. Tested on 3 sample and 8 real briefs; failure modes are in the repo.
  • TabPFN (tabpfn-v3.5, real weights, local CPU): the prediction half. The regressor's quantiles give the hours range and feed the price range, and the classifier gives the on-time probability, both fitted on the freelancer's own past jobs (30 synthetic rows in the demo). A note for Prior Labs: the browser login crashed on Windows with WinError 10038 because its prompt calls select on stdin; an API key in TABPFN_TOKEN worked.
πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.