Instructions to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M # Run inference directly in the terminal: llama cli -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M # Run inference directly in the terminal: llama cli -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama cli -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Use Docker
docker model run hf.co/JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
- Llama new
- LM Studio
- Jan
- vLLM
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "JobOpportunitiesAPI/joa-job-extractor-400m-gguf" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "JobOpportunitiesAPI/joa-job-extractor-400m-gguf", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
- Ollama
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with Ollama:
ollama run hf.co/JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
- Unsloth Desktop
- Pi
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with Docker Model Runner:
docker model run hf.co/JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
- Lemonade
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Run and chat with the model
lemonade run user.joa-job-extractor-400m-gguf-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use JobOpportunitiesAPI/joa-job-extractor-400m-gguf with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "JobOpportunitiesAPI/joa-job-extractor-400m-gguf:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
joa-job-extractor-400m-gguf
A small model that reads one job posting and returns 13 fields as a single JSON object: whether the posting is still open, title, company, location, salary (currency, minimum, maximum, period), posted date, whether the poster is an agency, remote type and seniority.
It is IBM Granite 3.1 1B-A400M (a mixture-of-experts model with 1B parameters in total and about 400M active per token) fine-tuned with a LoRA adapter, merged, and converted to GGUF for llama.cpp. It was built by Job Opportunities API to extract pay and other fields from public job postings on its own hardware.
Files (all three are the same model at different precision):
| file | size | sha256 |
|---|---|---|
| joa-job-extractor-400m-Q4_K_M.gguf | 0.82 GB | ff9e8b9071f6b56458be9716e2d93fea18a5ae40fc240d3ff141018beb3de4d0 |
| joa-job-extractor-400m-Q8_0.gguf | 1.42 GB | 95396a08a80a7c0541f6ef389dbf28cf301b98607c4301050ee05537d8e6ee03 |
| joa-job-extractor-400m-f16.gguf | 2.67 GB | c303b869da88311ba9b556fcba7610c698957f4fc250786f1cf91f3567c5bf05 |
| system_prompt.txt | 5.5 KB | the exact system prompt the model was trained on (see below) |
| examples/ | one real input and the model's real output |
Intended use
- Reading the text of a job posting that you have already fetched (an employer career page, an ATS posting, JSON-LD) and turning it into structured fields, in batch, on your own machine.
- Its strongest field is salary: it finds pay that rule-based parsers miss, including in German, French and Dutch postings (see the numbers below).
- A second pass after a rules-based parser, to recover salaries the rules did not find. This is how its authors intend to use it.
Not intended for: deciding anything about a person, scoring candidates, or extracting data from pages that are not job postings. It does not browse, it does not fetch pages, and it has no tools. It only reads the text you give it.
Prompt format
The model was trained on one fixed prompt, and it only works well with that prompt. Use it unchanged.
- System message: the contents of system_prompt.txt, with the placeholder
{today}replaced by the date the posting was fetched (YYYY-MM-DD). It is about 1.4K tokens. It defines the open or closed decision, the field rules (salary, location, seniority and so on) and the output format. - User message: an "evidence bundle", the text of the posting laid out in five sections, always in this order:
EVIDENCE BUNDLE for <posting URL>
fetched_at: <ISO timestamp> | ATS family: <name or "page">
Source priority: 1 rendered page > 2 JSON-LD > 3 ATS API > 4 liveness > 5 JOA stored text (may be stale).
## 1. RENDERED PAGE (what a candidate sees)
final_url: ...
http_status: 200
page <title>: ...
(meta lines, then)
--- page body ---
<visible text of the page>
## 2. JSON-LD JobPosting: <none on the page | key: value lines>
## 3. ATS API RECORDS
<none, or the posting's record from its applicant tracking system>
## 4. LIVENESS SIGNALS (deterministic; hint: ...)
requested_url: ...
final_url: ... (http 200)
## 5. JOA STORED TEXT (lowest priority - possibly stale; prefer sections 1-4)
<plain text of the posting>
Sections you do not have can say none or [not rendered: <reason>], as in the example below. Typical bundles are 4 to 5K tokens (median 4.8K in the training data), and the longest in the evaluations was about 17.7K tokens, so run it with a context of at least 24K.
- Sampling: temperature 0, max_tokens 1024. No tools, no chat template arguments (the model's own template from the GGUF is used).
Output schema
The reply is exactly one flat JSON object with these keys and no others:
| key | type | meaning |
|---|---|---|
| job_in_url | bool | true only if the page shows this specific job and it is still open |
| inactive_reason | string or null | when job_in_url is false: hard_404, soft_404, redirect_to_search, server_error, login_wall, wrong_job or expired_notice; otherwise null |
| title | string or null | the title as the posting states it |
| company | string or null | the employer as the posting states it |
| location | string or null | "City, Region, Country"; US states as two letters and country "USA"; a part left empty rather than invented, for example "Remote, , USA" |
| salary_currency | string or null | ISO code (USD, EUR, GBP); a bare "$" follows the job's country |
| salary_min | number or null | lower bound, or the single figure |
| salary_max | number or null | upper bound, or the single figure |
| salary_period | string or null | hour, day, week, month or year, only when a unit is stated |
| posted_date | string or null | dd/mm/yyyy, only when a date is stated |
| is_agency | bool or null | true if the poster is not the employer itself (job board, staffing firm) |
| remote_type | string or null | onsite, hybrid or remote |
| seniority | string or null | Entry Level (0 years), Entry Level (1 year), Mid (2-3 years), Senior (4+ years), Lead, Manager, Director or Executive |
When job_in_url is false, every other field is null. Not stated means null.
Worked example
This is a real public posting from a US city government career site (City of Fishers, Indiana), unchanged. The page itself could not be rendered because the site's robots.txt disallows it, so the bundle holds only the stored text. The model's answer is below the bundle, also unchanged. The same files are in the examples/ folder.
Input: the user message (evidence bundle)
EVIDENCE BUNDLE for https://cityoffishers.clearcompany.com/careers/jobs/fd1cffd1-14a9-dcfd-0db5-5bc530f02438/apply
fetched_at: 2026-11-03T19:39:48 | ATS family: page
Source priority: 1 rendered page > 2 JSON-LD > 3 ATS API > 4 liveness > 5 JOA stored text (may be stale).
## 1. RENDERED PAGE (what a candidate sees)
[not rendered: robots.txt disallows this URL]
## 2. JSON-LD JobPosting: none on the page
## 3. ATS API RECORDS
none (no known ATS recipe for this host and no job-matched JSON loaded by the page)
## 4. LIVENESS SIGNALS (deterministic; hint: no signal)
requested_url: https://cityoffishers.clearcompany.com/careers/jobs/fd1cffd1-14a9-dcfd-0db5-5bc530f02438/apply
## 5. JOA STORED TEXT (lowest priority - possibly stale; prefer sections 1-4)
Title: Part Time Laborer - DPW Streets
Status: Year-round Part-time (average 29 hours per week)
Compensation: $17 hourly
Description
This year-round part-time position performs general labor to help ensure city streets, right-of-ways, park grounds and facilities are maintained. Position will be required to work some evening, weekend and holiday hours including on-call snowplow/weather event duties as needed.
Fishers Department of Public Works (DPW) is responsible for the maintenance and upkeep of all municipal assets. These responsibilities include maintaining 340 center lane miles of roads and associated right-of-way which includes snow removal, crack sealing, patching, irrigation needs, traffic signals, striping, and street cleaning. DPW also maintains 1300 acres of parks and school grounds that includes over 60 baseball/softball fields, over 60 athletic fields, and over 500 acres of common area. From concerts to 5K runs, DPW staff is on the scene to provide setup and support.
Examples of Duties:
- Assist in curb and street repairs, asphalt and concrete work, etc.
- Trim trees and bushes along city roadways
- Pick up debris/trash along right-of-ways
- Maintain existing public open space areas, empty trash cans, etc.
- Assist in maintaining park managed facilities, athletic fields, irrigation systems and public playgrounds
- Clear brush, pull weed, rake, dig and perform miscellaneous gardening duties in and around municipal facilities and grounds
- Clean public restrooms at various city parks and buildings
- Mowing and weed trimming
- Assist on sanitary/storm sewer cleaning equipment
- Set up of tables, chairs, equipment, etc. for special events
- Clean shop and equipment as necessary
- Assist with snowplow / weather event duties
Minimum Qualifications:
- High school diploma or equivalent;
- Must have valid driver's license and ability to be insured to drive a city vehicle which includes at least 3 years of driving history outside of permit time;
- Must be able and willing to work outside in all weather conditions;
- Position will be required to work some evening, weekend and holiday hours;
- Ability to lift and carry objects weighing up to 50 lbs;
- Ability to climb ladders and maintain balance;
- Ability to interact and communicate effectively with a variety of people;
- Able to handle several projects at the same time;
- Reliable and trustworthy;
- Ability to work independently and within a team.
Model output (the system message is system_prompt.txt with {today} set to 2026-11-03):
{"job_in_url": true, "inactive_reason": null, "title": "Part Time Laborer - DPW Streets", "company": "Fishers Department of Public Works", "location": "Fishers, IN, USA", "salary_currency": "USD", "salary_min": 17, "salary_max": 17, "salary_period": "hour", "posted_date": null, "is_agency": false, "remote_type": "onsite", "seniority": "Entry Level (0 years)"}
The pay ($17 hourly) became salary_currency USD, salary_min and salary_max 17, salary_period hour. The posting states no date, so posted_date is null. The seniority value is the model's inference, since the text does not state one: check it before you rely on it.
Run it with llama.cpp
# 1. Start a server (llama.cpp, any recent build). --cache-ram 0 keeps memory low.
llama-server -m joa-job-extractor-400m-Q4_K_M.gguf -c 24576 -np 1 --jinja --cache-ram 0 --port 8091
# 2. Build the request from system_prompt.txt and your evidence bundle, then post it.
BUNDLE=example_bundle.txt # your own bundle in the layout above
DATE=$(grep -m1 -o 'fetched_at: [0-9-]*' "$BUNDLE" | cut -d' ' -f2)
SYS=$(cat system_prompt.txt); SYS=${SYS//\{today\}/$DATE}
jq -n --arg s "$SYS" --rawfile b "$BUNDLE" \
'{messages:[{role:"system",content:$s},{role:"user",content:$b}],temperature:0,max_tokens:1024}' |
curl -s http://127.0.0.1:8091/v1/chat/completions -H 'content-type: application/json' -d @- |
jq -r '.choices[0].message.content' | jq .
Any OpenAI compatible client works the same way. Parse the reply as JSON and take the last top-level object that has a job_in_url key.
Evaluation
All numbers come from Job Opportunities API's own tests, run in November 2026. They are not an independent benchmark.
Test set "t100". 100 held-out postings with a reference labelling, scored with a strict field-by-field F1 (a field counts only when it matches exactly after normalisation). The fully trained adapter served with vLLM on a GPU scored a strict F1 of 0.926 (95% interval 0.902 to 0.948, precision 0.933, recall 0.919, 100 of 100 outputs parsed).
The GGUF files, run with llama.cpp on a GTX 1070 (8 GB, context 16384) on the same 100 postings:
| file | strict F1 (95% interval) | precision | recall |
|---|---|---|---|
| Q4_K_M | 0.923 (0.895 to 0.946) | 0.944 | 0.902 |
| Q8_0 | 0.922 (0.896 to 0.946) | 0.935 | 0.910 |
| f16 | 0.918 (0.892 to 0.942) | 0.930 | 0.907 |
The three are statistically the same as the GPU result, so Q4_K_M is the practical choice. One posting of 17.7K tokens did not fit the 16K context used in this test and counted as a miss (99 of 100 parsed); a larger context removes that.
Salary on a wider audit. 1,084 postings from 74 job sources, 886 of them checked against the source's own data (vendor API pay fields, JSON-LD, or the visible text). On those 886, for postings whose source states a salary (261):
| recall | precision | F1 | |
|---|---|---|---|
| this model, salary | 86 | 94 | 90 |
| the deterministic parser it is compared with, salary | 59 | 97 | 74 |
The model found pay in many postings where the parser found none, including pay written in prose and pay in German, French and Dutch postings. The parser is more precise. On the audit, all 1,084 outputs were valid JSON (0 malformed, including 123 long bundles of over 6,000 tokens).
Other fields on the same audit (F1, 886 postings): title 99, company 97, city 95, remote type 90, posted date 97, salary currency 94, salary period 95. Weaker: country 91 and seniority 64 (see limits).
Speed (one posting at a time, about 4.5K prompt tokens and about 130 generated tokens per posting):
| hardware | file | seconds per posting, median |
|---|---|---|
| GTX 1070 | Q8_0 | 2.1 |
| GTX 1070 | Q4_K_M | 2.3 |
| 6 busy CPU threads | Q4_K_M | 29 |
| 2 CPU threads | Q4_K_M | 70 |
On a CPU this is a batch tool, not an interactive one: the long input dominates the time.
Known limits
- Invented amounts. About 3 percent of the salary amounts it returned (8 of 271) did not appear in the posting text as written. Always check that the number appears in the posting before you store it. The usual rule is: accept a salary only if its digits can be found in the source text.
- Currency and period can disagree. When a posting gives both an annual and an hourly figure, the model can mix them (for example an annual amount labelled "hour"). Check that the period fits the amount.
- Country and seniority are weak. Country is free text and is sometimes missing or wrong (F1 91 against 99 for a rules-based reader). Seniority answers by years of experience, and on the audit it scored 64 against 88 for a title-based rule; part of that gap is a difference in definition, but treat the field as a hint.
- Not all fields are covered. Employment type (full time, part time) and region are not outputs.
- Languages. The training postings are mostly English. German, French and Dutch were tested on small samples (60, 47 and 19 postings) and worked for salary; other languages were not measured.
- Input matters. The model reads the bundle you give it. If the page text was not captured (script-only pages, blocked pages), there is nothing to extract, and the correct answer is null fields.
- No tools, no browsing. It cannot confirm that a posting is still open on the live web; job_in_url is a judgement from the text and signals in the bundle.
- It was fine-tuned on one fixed prompt. A different prompt or a different bundle layout will lower quality.
Training data and method
- Data. About 47,300 training examples. Each is a public job posting from an employer's own career site or an applicant tracking system board (Greenhouse, Lever, Workday, SmartRecruiters and similar), laid out as an evidence bundle, with personal contact details redacted. A further 1,000 postings were held out, and the 100 postings of the test set above were excluded from training. The data is not released.
- Labels. Produced by a larger teacher model, zai-org/GLM-5.3-Flash (MIT licence), reading each bundle and answering in the output schema above. A grounding filter then removed or corrected values that the posting text did not support. The labels were not checked by hand one by one.
- Fine-tuning. LoRA on the attention projections (q, k, v, o), rank 64, alpha 128, one epoch, learning rate 1e-4, starting from ibm-granite/granite-3.1-1b-a400m-instruct. The experts of the mixture-of-experts layers were not adapted. The adapter was merged into the base weights and converted with llama.cpp (
convert_hf_to_gguf.pyto f16, thenllama-quantizefor Q8_0 and Q4_K_M). Training ran on the EuroHPC Discoverer+ cluster.
License
The model is released under the Apache License 2.0, like its base model, IBM Granite 3.1. The teacher model that produced the labels is MIT licensed. Check the terms of the websites you fetch postings from before you use their content.
Who made it
Job Opportunities API, https://jobopportunitiesapi.org. Questions and feedback: hello@jobopportunitiesapi.org.
- Downloads last month
- -
4-bit
8-bit
16-bit
Model tree for JobOpportunitiesAPI/joa-job-extractor-400m-gguf
Base model
ibm-granite/granite-3.1-1b-a400m-base