git:20260808.ebac469 to git:20260816.7bd899c

78 added, 2 removed. Audit A to A.

---
name: wikimedia-ml-services
- description: Score article quality, revert risk, edit quality (goodfaith/damaging), readability, topic classification, reference quality, language identification, content translation recommendations, article descriptions, and article country using Wikimedia ML inference APIs (Lift Wing and legacy ORES)
+ description: Score article quality, revert risk, edit quality, readability, topics, references, language ID, and article country via Wikimedia ML inference APIs (Lift Wing + legacy ORES), plus LiftWing LLM chat completions (Qwen3, OpenAI-compatible)
depends_on: [wikimedia-api-access]
license: MIT
compatibility: opencode
skill_discovery_hints:
- keywords: ["revert", "vandalism", "quality", "FA", "GA", "Start", "Stub", "article quality", "goodfaith", "damaging"]
- keywords: ["readability", "grade level", "Flesch-Kincaid", "reading score"]
- keywords: ["topic", "classification", "category prediction", "outlink", "article topic"]
- keywords: ["Lift Wing", "ORES", "ML", "machine learning", "inference", "model score"]
- keywords: ["reference", "citation quality", "reference need", "unsourced", "reference risk"]
- keywords: ["language detection", "langid", "identify language"]
- keywords: ["translation", "content translation", "cross-language", "recommendation"]
- last_verified: 2026-06-10
+ - keywords: ["LLM", "chat completions", "Qwen", "OpenAI-compatible", "text generation", "LiftWing Studio", "large language model"]
+ last_verified: 2026-08-14
---
> ⚠️ **User-Agent required:** All API calls below need a descriptive `User-Agent` header. See the **[wikimedia-api-access](../wikimedia-api-access/SKILL.md)** skill for the correct format and rate-limiting patterns.
## Context: Two Eras of Wikimedia ML Services
Wikimedia has two ML inference pipelines:
| Era | Status | Architecture | Base URL |
|-----|--------|-------------|----------|
| **ORES** | **Deprecated** (unmaintained since late 2023, may break) | Python scoring server, GET-based, batch scoring | `https://ores.wikimedia.org/v3/scores/{wiki}/{revid}` |
| **Lift Wing** | **Active** (current WMF ML platform) | Kubernetes/KServe, POST-based, one model per endpoint | `https://api.wikimedia.org/service/lw/inference/v1/models/{model-name}:predict` |
**Always prefer Lift Wing.** ORES is documented here *only* for migrating existing code. New tools must use Lift Wing.
---
## Reference: Available Lift Wing Models
Lift Wing serves two tiers of models:
### Revscoring Models (Frozen — migrated from ORES, no further improvements)
| Model Name | Input | Output | Purpose | Modern Replacement |
|-----------|-------|--------|---------|-------------------|
| `{wiki}-goodfaith` | `rev_id` | `{"prediction": true/false, "probability": {"true": float, "false": float}}` | Good/bad faith edit | `revertrisk-*` |
| `{wiki}-damaging` | `rev_id` | Same structure | Is edit damaging? | `revertrisk-*` |
| `{wiki}-reverted` | `rev_id` | Same structure | Will edit be reverted? | `revertrisk-*` |
| `{wiki}-articlequality` | `rev_id` | `{"prediction": "FA"/"GA"/"B"/"C"/"Start"/"Stub", "probability": {...}}` | Article quality class | `articlequality` (modern model, continuous score) |
| `{wiki}-draftquality` | `rev_id` | `{"prediction": "...", "probability": {...}}` | Draft quality | — |
| `{wiki}-articletopic` | `rev_id` | `{"prediction": [...], "probability": {...}}` | Article topic scores | `outlink-topic-model` |
| `{wiki}-drafttopic` | `rev_id` | Same structure | Draft topic scores | — |
| `wikidatawiki-itemquality` | `rev_id` | Same structure | Wikidata item quality | — |
| `wikidatawiki-itemtopic` | `rev_id` | Same structure | Wikidata item topic | — |
Replace `{wiki}` with the wiki code (e.g., `enwiki`, `frwiki`, `arwiki`). Available wikis vary per model — check the [model list](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing#Revscoring_models_(migrated_from_ORES)).
### Modern Models (Actively developed, preferred for new tools)
| Model Name | Input | Output (Key Fields) | Best For |
|-----------|-------|-------------------|----------|
| `revertrisk-language-agnostic` | `{"rev_id": int, "lang": string}` | `output.prediction` (bool), `output.probabilities.{true, false}` (float) | **Anti-vandalism** — single score replacing goodfaith+damaging combined |
| `revertrisk-multilingual` | `{"rev_id": int, "lang": string}` | `output.prediction` (bool), `output.probabilities.{true, false}` (float) | Anti-vandalism across 300+ languages |
| `revertrisk-wikidata` | `{"rev_id": int}` | `output.prediction` (bool), `output.probabilities.{true, false}` (float) | Wikidata-specific revert risk |
| `readability` | `{"rev_id": int, "lang": string}` | `output.score` (float 0–1), `output.fk_score_proxy` (grade level) | **Readability scoring** — complexity and grade level |
| `reference-need` | `{"rev_id": int, "lang": string}` | `reference_need_score` (float 0–1) | **Find unsourced claims** in an article |
| `reference-risk` | `{"rev_id": int, "lang": string}` | `reference_risk_score` (float), `reference_count` (int), `survival_ratio` (dict) | **Reference quality risk** — are citations reliable? |
| `outlink-topic-model` | `{"page_title": string, "lang": string}` | `prediction.results[]` — array of `{topic: string, score: float}` | **Topic classification** — replaces articletopic, link-based |
| `articlequality` | `{"rev_id": int, "lang": string}` | `score` (float 0–1) | **Quality scoring** — continuous quality score (modern model, different from Revscoring grades) |
| `article-country` | `{"title": string, "lang": string}` | `prediction.results[]` — array of `{country: string, score: float, source: {...}}` | Predict which country an article is about |
| `article-descriptions` | `{"title": string, "lang": string, "num_beams": int}` | (generates Wikidata-style short description — heavy model, may be slow) | Generate a Wikidata-like short description |
| `langid` | `{"text": string}` | `wikicode` (string), `score` (float confidence) | **Language identification** of raw text |
> 💡 **Prefer modern models.** The Revscoring models are frozen — no retraining, no new wikis. The modern models (especially `revertrisk-*`, `outlink-topic-model`) are trained on fresher data and cover more wikis.
> ⚠️ **Model naming is inconsistent.** The modern `articlequality` model is called just `articlequality` (not `articlequality-language-agnostic`). The content translation service is a **GET REST API** at `/service/lw/recommendation/api/v1/translation`, not a POST inference model. See the individual SOPs below.
+
+ ---
+
+ ## Reference: LiftWing Large Language Models (chat completions)
+
+ LiftWing also hosts **open-weight LLM chat models** — a different service from
+ the scoring models above (OpenAI-compatible chat API, not `:predict` POSTs).
+ Quickstart: the [Wikimania 2026 page](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing/Large_Language_Models/Wikimania_2026);
+ full platform reference: the [main LLM docs](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing/Large_Language_Models).
+
+ | Model | Card | Context | Notes |
+ |---|---|---|---|
+ | `llm-qwen3-14b` | [Qwen3-14B-FP8](https://huggingface.co/Qwen/Qwen3-14B-FP8) | 16K tokens | 14B general-purpose chat model; good default |
+ | `llm-qwen36-27b` | [Qwen3.6-27B-FP8](https://huggingface.co/Qwen/Qwen3.6-27B-FP8) | 32K tokens | 27B chat model; largest available |
+
+ Both multilingual and instruction-tuned; responses stream token-by-token.
+
+ ### Endpoint (OpenAI-compatible, no API key)
+
+ ```
+ https://api.wikimedia.org/service/lw/inference/v1/models/llm-<model>/openai/v1/chat/completions
+ ```
+
+ - ⚠️ **Model name must match the URL path** — the `model` in the JSON body
+ must equal the `llm-<model>` segment in the URL; a mismatch 404s.
+ - No API key: any OpenAI client works by pointing `base_url` at the model
+ endpoint (the URL above minus `/chat/completions`) and using
+ `api_key="none"`.
+ - Responses may start with a `<think>…</think>` reasoning block — strip it
+ if you only want the final answer.
+ - `stream=True` is supported (token-by-token output).
+
+ ```python
+ from openai import OpenAI
+ client = OpenAI(
+ base_url="https://api.wikimedia.org/service/lw/inference/v1/models/llm-qwen3-14b/openai/v1",
+ api_key="none", # public endpoint; no key required
+ )
+ resp = client.chat.completions.create(
+ model="llm-qwen3-14b",
+ messages=[{"role": "user", "content": "Explain vLLM in one sentence."}],
+ stream=True,
+ )
+ for chunk in resp:
+ print(chunk.choices[0].delta.content or "", end="")
+ ```
+
+ ### Rate limits & access
+
+ - **Anonymous public access: 100 requests/hour** — counted per client,
+ shared across all models; over the limit → HTTP 429 until the window
+ resets. No API key required.
+ - **From Toolforge: effectively unlimited** (automatic — no request
+ needed). The standard path for tools and hackathon apps.
+ - Experimental platform: **no SLA**; models/endpoints can change without
+ notice.
+
+ ### LiftWing Studio (no-code chat UI)
+
+ [LiftWing Studio](https://liftwing-studio.wmcloud.org/) is an Open WebUI
+ chat interface: register an account (activated after **admin approval**),
+ then chat with model selection, chat history, and document uploads.
+ Privacy differs from the API: **Studio saves your chats** by default (use a
+ temporary chat to avoid persistence); the API itself persists nothing — no
+ logging, retention, or training.
+
+ ### Capabilities
+
+ Works today: chat & text completions (OpenAI-compatible), streaming,
+ multilingual input, context up to 32K tokens. **Not available yet:** tool /
+ function calling, web search / browsing, server-side RAG, vision / multimodal.
+ DIY RAG works — fetch the context yourself and include it in the prompt.
+ Models have fixed training cutoffs: retrieve current facts rather than
+ trusting model memory, and verify before any on-wiki use (outputs are
+ assistive drafts; bot/editing policies still apply).
---
## SOP: Making Lift Wing API Calls
### Common Request Pattern
All Lift Wing models use `POST` to the same base URL pattern:
```
POST https://api.wikimedia.org/service/lw/inference/v1/models/{MODEL_NAME}:predict
Content-Type: application/json
Authorization: Bearer {TOKEN} (optional — needed for >50k req/hr)
User-Agent: MyBot/1.0 (user@example.com) ContentGapResearch
```
```python
import requests
headers = {
"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch",
"Content-Type": "application/json",
}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict"
data = {"rev_id": 123456789}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
```
### Authentication for Higher Rate Limits
| Client Type | Rate Limit (req/h) | Rate Limit (req/s) | How to Qualify |
|-------------|-------------------|-------------------|----------------|
| Anonymous | 50,000 | 15 | No auth needed |
| Browser session | 50,000 | 50 | Login cookie |
| **Authenticated** | **100,000** | **100** | OAuth token via `api.wikimedia.org/wiki/Authentication` |
| **Known client** (Toolforge, approved bots) | **200,000** | **200** | OAuth token + known user-agent |
To authenticate, follow the [Wikimedia API authentication guide](https://api.wikimedia.org/wiki/Authentication) to obtain an OAuth access token, then include it in requests:
```python
headers = {
"Authorization": f"Bearer {access_token}",
"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch",
"Content-Type": "application/json",
}
```
### Handling Response Structure
> ⚠️ **This is the single most common source of bugs.** The response formats are nested and non-obvious. Always check the response structure before writing access code.
**Revscoring models** on Lift Wing use the **old ORES response format** (not the Lift Wing envelope):
```json
{
"enwiki": { # ← key is the wiki code (changes per model!)
"models": {
"articlequality": {"version": "0.9.2"}
},
"scores": {
"123456789": { # ← key is the revision ID as a STRING
"articlequality": { # ← key is the model name
"score": {
"prediction": "C", # ← "FA"/"GA"/"B"/"C"/"Start"/"Stub"
"probability": { # ← per-class confidence (sums to 1.0)
"B": 0.02, "C": 0.78, "FA": 0.0,
"GA": 0.12, "Start": 0.07, "Stub": 0.01
}
}
}
}
}
}
}
```
**Access pattern:**
```python
result[wiki]["scores"][str(rev_id)][model_name]["score"]["prediction"]
# Example:
grade = result["enwiki"]["scores"]["123456789"]["articlequality"]["score"]["prediction"]
```
**Modern revert-risk models** use a flat unified envelope:
```json
{
"model_name": "revertrisk-language-agnostic",
"model_version": "3",
"wiki_db": "enwiki",
"revision_id": 123456789,
"output": {
"prediction": false, # ← bool: true = likely reverted
"probabilities": {
"true": 0.34, # ← float 0-1: probability of revert
"false": 0.66
}
}
}
```
**Access pattern:**
```python
risk = result["output"]["probabilities"]["true"] # ← float 0-1
prediction = result["output"]["prediction"] # ← bool
```
**Modern articlequality model (continuous score):**
```json
{
"model_name": "articlequality",
"model_version": "1",
"wiki_db": "enwiki",
"revision_id": 123456789,
"output": {
"prediction": {"score": 0.72} # ← float 0-1, NOT a discrete grade
}
}
```
**Access pattern:**
```python
score = result["output"]["prediction"]["score"] # ← float 0-1
# 0.72 is closer to "B" grade (the continuous model is different from the discrete Revscoring model!)
```
**Modern models** have per-model response schemas. See the model-specific SOPs below.
---
## SOP: Editing the Page — How Lift Wing and EventStreams Work Together
Lift Wing ML predictions integrate naturally with the EventStreams real-time feed. The most common production pattern is:
1. **Consume** `recentchange` or `revision-create` events via EventStreams (see the **[wikimedia-eventstreams](../wikimedia-eventstreams/SKILL.md)** skill)
2. **Score** each new revision via Lift Wing (revert risk, damaging, goodfaith)
3. **Act** on the score (flag for review, auto-revert, pass through)
```python
import requests, json
HEADERS = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
def score_revision_revscoring(wiki: str, rev_id: int, model: str) -> dict:
"""Score a single revision using a Revscoring model on Lift Wing.
Note: Revscoring models return the old ORES response format.
"""
url = f"https://api.wikimedia.org/service/lw/inference/v1/models/{wiki}-{model}:predict"
resp = requests.post(url, json={"rev_id": rev_id}, headers=HEADERS, timeout=30)
resp.raise_for_status()
return resp.json()
# Example: score an edit for goodfaith
result = score_revision_revscoring("enwiki", 123456789, "goodfaith")
prediction = result["enwiki"]["scores"]["123456789"]["goodfaith"]["score"]["prediction"]
confidence = result["enwiki"]["scores"]["123456789"]["goodfaith"]["score"]["probability"]["true"]
# Modern models use a flat output structure — see per-model SOPs below
```
---
## SOP: Revert Risk Scoring (Anti-Vandalism)
The `revertrisk-*` models are the **recommended** replacement for the older `goodfaith` + `damaging` pair. They provide a single probability score for whether an edit will be reverted.
### Language-Agnostic Revert Risk
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/revertrisk-language-agnostic:predict"
data = {"rev_id": 123456789, "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# result["output"]["prediction"] is a bool (false = safe, true = likely reverted)
# result["output"]["probabilities"]["true"] is a float 0.0–1.0
```
### Multilingual Revert Risk
Same pattern, different model name — covers 300+ languages with a single model:
```python
url = "https://api.wikimedia.org/service/lw/inference/v1/models/revertrisk-multilingual:predict"
data = {"rev_id": 123456789, "lang": "en"}
```
### Threshold Guidance
| Revert Risk Score | Action |
|-------------------|--------|
| `< 0.3` | Low risk — pass through |
| `0.3 – 0.7` | Medium risk — flag for human review |
| `> 0.7` | High risk — escalate (tag, auto-revert if confident) |
Calibrate thresholds to your tool's precision/recall requirements. The model card provides precision-recall curves: [Language-agnostic revert risk model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Language-agnostic_revert_risk).
> 🎮 **Try it:** `python3 assets/patrol_simulator.py Albert_Einstein` — simulates patrolling the latest edit and produces a verdict.
---
## SOP: Article Quality Scoring
### With Revscoring Model (Frozen — uses ORES response format)
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-articlequality:predict"
data = {"rev_id": 123456789}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
scores = result["enwiki"]["scores"]["123456789"]["articlequality"]["score"]
grade = scores["prediction"] # "FA", "GA", "B", "C", "Start", "Stub"
probs = scores["probability"] # {"FA": 0.0, "GA": 0.12, "B": 0.02, ...}
```
> 💡 The modern `articlequality` model provides a **continuous quality score** (float 0–1), while the Revscoring `{wiki}-articlequality` model provides a **discrete grade** (FA/GA/B/C/Start/Stub). The modern model name is just `articlequality` — not `articlequality-language-agnostic`.
### Integration with Page Assessment
Combine ML quality scores with WikiProject assessment data from the **[wikimedia-page-assessment](../wikimedia-page-assessment/SKILL.md)** skill:
```python
# ML quality — available for any wiki
ml_grade = scores["prediction"]
# WikiProject quality — only for wikis with PageAssessments (enwiki, frwiki, etc.)
# query: SELECT pa_class FROM page_assessments WHERE pa_page_id = {page_id}
```
Use ML scores as a **fallback** when PageAssessments are not available (most wikis don't have the extension), or as a second opinion for unassessed articles.
> 🎮 **Try it:** `python3 assets/article_quality_report.py Albert_Einstein en` — generates a full report with quality + readability + topics + reference risk.
---
## SOP: Topic Classification
The `outlink-topic-model` replaces the older `articletopic` model. It uses the article's outlinks to predict topics and is available for more wikis.
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/outlink-topic-model:predict"
data = {"page_title": "Douglas_Adams", "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# result["prediction"]["results"] is an array of {topic, score} objects:
# [
# {"topic": "STEM.STEM*", "score": 0.90},
# {"topic": "Culture.Biography.Biography*", "score": 0.73},
# ...
# ]
topics = {r["topic"]: r["score"] for r in result["prediction"]["results"]}
```
### Topic Hierarchy
The outlink topic model uses a hierarchical taxonomy. Topics are dot-separated paths (e.g., `Culture.Media.Books`, `STEM.Science.Physics`). The top-level categories are:
| Top-Level | Examples |
|-----------|----------|
| `Culture` | Media, Books, Television, Music, Sports, Food |
| `Geography` | Regions, Countries, Cities |
| `History` | Events, People, Periods |
| `People` | Biographical topics |
| `STEM` | Science, Technology, Engineering, Mathematics |
| `Society` | Law, Education, Politics, Religion |
Interpret scores by their top-level prefix for broad categorization, or drill into subcategories for fine-grained classification.
> 🎮 **Try it:** `bash scripts/playground.sh` and choose option 4 — interactive topic classification.
---
## SOP: Readability Scoring
The `readability` model scores how readable an article's content is using standard metrics.
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/readability:predict"
data = {"rev_id": 123456789, "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# Takes rev_id (not page_title). Returns:
# result["output"]["score"] is a float 0–1 readability score
# result["output"]["fk_score_proxy"] approximates Flesch-Kincaid grade level
score = result["output"]["score"]
grade = result["output"]["fk_score_proxy"]
```
### Interpreting Readability Scores
| Field | Range | Interpretation |
|-------|-------|----------------|
| `output.score` | 0–1 | Higher = more readable. >0.6 = accessible. <0.3 = very difficult. |
| `output.fk_score_proxy` | 0–20+ | US grade level (approximate). 8–10 = average adult. >14 = complex academic text. |
> 🎮 **Try it:** `python3 assets/article_quality_report.py Albert_Einstein en` includes a readability section.
---
## SOP: Reference Quality Scoring
Two models help assess citation quality in articles. Both use `rev_id` + `lang` (not `page_title`).
### Reference Need (Find Unsourced Claims)
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/reference-need:predict"
data = {"rev_id": 123456789, "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# Returns a single reference_need_score (float 0–1), not per-sentence breakdown:
# result["reference_need_score"] — higher = more claims need citations
# result["model_version"] — model version
score = result["reference_need_score"]
```
### Reference Risk (Citation Quality)
```python
url = "https://api.wikimedia.org/service/lw/inference/v1/models/reference-risk:predict"
data = {"rev_id": 123456789, "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# Returns:
# result["reference_risk_score"] — float, higher = riskier citations
# result["reference_count"] — int, number of references in the revision
# result["survival_ratio"] — dict with min/mean/median citation survival
risk = result["reference_risk_score"]
ref_count = result["reference_count"]
```
---
## SOP: Language Identification
The `langid` model predicts what language a piece of text is written in. It takes **raw text** (not a page reference).
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/langid:predict"
data = {"text": "Albert Einstein was a German-born theoretical physicist."}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# result["wikicode"] is the language code, e.g., "en"
# result["score"] is the confidence (float 0–1)
# result["language"] is the full language name, e.g., "English"
# result["languagename"] is an alternative name
```
To get the text of a Wikipedia article for language identification, first fetch the page content via the [REST API](../wikimedia-api-access/SKILL.md).
> 🎮 **Try it:** `python3 assets/language_explorer.py` — interactive REPL. Type any text and see the detected language. Or `python3 assets/language_explorer.py --samples` to test 10 languages at once.
---
## SOP: Article Country Prediction
The `article-country` model predicts which countries an article is about, with confidence scores and evidence sources.
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/inference/v1/models/article-country:predict"
data = {"title": "Albert_Einstein", "lang": "en"}
resp = requests.post(url, json=data, headers=headers, timeout=30)
result = resp.json()
# result["prediction"]["results"] is an array sorted by score (descending):
# [
# {"country": "United States", "score": 1.0, "source": {"wikidata_properties": [...], "categories": [...]}},
# {"country": "Switzerland", "score": 0.67, "source": {...}},
# ...
# ]
top_country = result["prediction"]["results"][0]["country"]
all_countries = {r["country"]: r["score"] for r in result["prediction"]["results"]}
```
Note: uses `title` (not `page_title`) as the parameter name.
---
## SOP: Content Translation Recommendations
The content translation recommendation is a **GET-based REST API** (not a POST inference model).
```python
import requests
headers = {"User-Agent": "MyBot/1.0 (user@example.com) ContentGapResearch"}
url = "https://api.wikimedia.org/service/lw/recommendation/api/v1/translation"
params = {
"source": "en", # Source wiki language code
"target": "fr", # Target wiki language code
"count": 3, # Number of recommendations (default 12)
"seed": "Apple", # Seed article for personalized recommendations
}
resp = requests.get(url, params=params, headers=headers, timeout=30)
result = resp.json()
# result["recommendations"] is an array of:
# {"title": "Flamenco (apple)", "pageviews": 0, "wikidata_id": "Q19597233",
# "rank": 7, "langlinks_count": 1, "size": 6871, ...}
```
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `source` | string | ✅ | Source wiki language code (e.g., `en`) |
| `target` | string | ✅ | Target wiki language code (e.g., `fr`) |
| `count` | int | ❌ | Number of recommendations (default `12`) |
| `seed` | string | ❌ | Seed article title for personalized recommendations (pipe `|`-separated for multiple) |
| `topic` | string | ❌ | Article topic filter (pipe `|`-separated) |
| `include_pageviews` | bool | ❌ | Include pageview counts (`true`/`false`, default `false`) |
| `search_algorithm` | string | ❌ | `morelike` or `mostpopular` (default `morelike`) |
---
## SOP: Migrating from ORES (Legacy Code)
If you encounter code that calls ORES, here is the migration pattern:
### ORES Call (Legacy)
```bash
# ORES — GET, batch multiple models in one call
curl -s -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://ores.wikimedia.org/v3/scores/enwiki/12345?models=goodfaith|damaging'
```
### Equivalent Lift Wing Calls
```bash
# Lift Wing — POST, one call per model
curl -s -X POST -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-goodfaith:predict' \
-H 'Content-Type: application/json' \
-d '{"rev_id": 12345}'
curl -s -X POST -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-damaging:predict' \
-H 'Content-Type: application/json' \
-d '{"rev_id": 12345}'
```
### ORES → Lift Wing Feature Extraction
```bash
# ORES could return features with ?features=True
curl -s -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://ores.wikimedia.org/v3/scores/enwiki/12345/goodfaith?features=True'
# Lift Wing equivalent
curl -s -X POST -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://api.wikimedia.org/service/lw/inference/v1/models/enwiki-goodfaith:predict' \
-H 'Content-Type: application/json' \
-d '{"rev_id": 12345, "extended_output": "True"}'
```
### ORES → Modern Revert Risk
```bash
# Old: two calls for goodfaith + damaging
curl -s -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://ores.wikimedia.org/v3/scores/enwiki/12345?models=goodfaith|damaging'
# New: single call to revertrisk with language parameter
curl -s -X POST -H "User-Agent: MyBot/1.0 (user@example.com) LiftWingMigration" \
'https://api.wikimedia.org/service/lw/inference/v1/models/revertrisk-language-agnostic:predict' \
-H 'Content-Type: application/json' \
-d '{"rev_id": 12345, "lang": "en"}'
```
### Key ORES → Lift Wing Differences
| Aspect | ORES | Lift Wing |
|--------|------|-----------|
| HTTP method | GET | POST |
| Batch support | Yes (multiple models, multiple revisions in one call) | No (one model per call) |
| Caching | Yes (pre-cached scores for recent edits) | **No** (must implement your own cache) |
| Features | `?features=True` | `"extended_output": "True"` (per model) |
| Auth | None | OAuth for higher rate limits |
| Response | `{"wiki": {"models": {"model": {"scores": {...}}}}}` | Per-model response (see individual SOPs) |
---
## SOP: Implementing Caching (Lift Wing Has None)
Lift Wing does **not** cache predictions (unlike ORES which pre-cached scores). If you score the same revision twice, you pay two inference calls. For production tools, implement your own cache:
```python
import time
class MLScoreCache:
def __init__(self, ttl_seconds: int = 3600):
self._cache = {}
self._ttl = ttl_seconds
def get(self, model: str, rev_id: int) -> dict | None:
key = (model, rev_id)
if key in self._cache:
entry = self._cache[key]
if time.time() - entry["ts"] < self._ttl:
return entry["data"]
del self._cache[key]
return None
def set(self, model: str, rev_id: int, data: dict):
self._cache[(model, rev_id)] = {"data": data, "ts": time.time()}
# Usage
cache = MLScoreCache()
def scored_revision(wiki, rev_id):
cached = cache.get(f"{wiki}-goodfaith", rev_id)
if cached:
return cached
result = call_liftwing(f"{wiki}-goodfaith", {"rev_id": rev_id})
cache.set(f"{wiki}-goodfaith", rev_id, result)
return result
```
---
## Guardrails
### ❌ Don't use ORES for new tools
ORES is **deprecated**. Always use Lift Wing. The ORES service could stop working at any time without notice.
### ❌ Don't use frozen Revscoring models when modern alternatives exist
- Use `revertrisk-*` instead of `goodfaith` + `damaging`
- Use `outlink-topic-model` instead of `articletopic`
The frozen models won't be retrained, won't gain new language support, and their accuracy degrades over time (model drift).
### ❌ Don't assume all models cover all wikis
Revscoring models only cover the wikis they were trained on. Check the [model list](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing#Revscoring_models_(migrated_from_ORES)) before using a wiki-specific model. Modern models (`revertrisk-multilingual`, `outlink-topic-model`) are language-agnostic and cover more wikis.
### ❌ Don't ignore model cards
Every model has a model card on Meta-Wiki that documents:
- Training data and methodology
- Performance metrics (precision, recall, F1)
- Known biases and limitations
- Appropriate use cases
Read the model card before using a model in production. Links are in the reference table below.
### ⚠️ Implement caching for repeated scoring
Since Lift Wing has no caching, scoring the same revision multiple times wastes rate limit quota and adds latency. Use an in-memory cache (as shown above) for short-lived tools, or a persistent cache (Redis, SQLite) for long-running services.
### ⚠️ Respect per-second rate limits, not just per-hour
The per-second limits (15 req/s anonymous, 100 req/s authenticated) are tighter than the per-hour limits. For batch operations:
- Add `time.sleep(0.1)` between requests (10 req/s)
- Use `requests.Session()` for connection reuse
- Implement exponential backoff on 429 responses
### ⚠️ Some models are internal-only
Models in the "experimental" Kubernetes namespace are only accessible from WMF production infrastructure. The externally accessible models are documented in the [API reference](https://api.wikimedia.org/wiki/Lift_Wing_API/Reference). If a model is not listed there, it's internal.
---
## Model Card Reference
| Model | Model Card |
|-------|------------|
| Language-agnostic revert risk | [Model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Language-agnostic_revert_risk) |
| Multilingual revert risk | [Model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Multilingual_revert_risk) |
| RevertRisk Wikidata | [Model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/RevertRisk_Wikidata) |
| Outlink-based article topic | [Model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/Language_agnostic_link-based_article_topic) |
| Article descriptions | [Model cards (description generation)](https://meta.wikimedia.org/wiki/Machine_learning_models) |
| Logo detection | [Model card](https://meta.wikimedia.org/wiki/Machine_learning_models/Production/gogologo) |
---
## Known Limitations
| Limitation | Affected Models | Impact | Workaround |
|------------|:---------------:|--------|------------|
| **Revert-risk models return HTTP 422 for revision 1.** New pages have no parent revision to compare against, so the model cannot compute a score. | `revertrisk-language-agnostic`, `revertrisk-multilingual`, `revertrisk-wikidata` | Cannot score brand-new page creations — a common use case for real-time patrol tools. | Use the `articlequality` model as a fallback for new pages (it scores the revision itself, not the diff). Note that `articlequality` has ~60s processing latency (see below). |
| **Articlequality model has ~60s processing latency.** Newly created revisions are not immediately available for scoring. The model returns empty scores for revisions less than ~60 seconds old. | `articlequality`, `{wiki}-articlequality` | Real-time patrol tools may get empty scores for very new pages. | Retry with exponential backoff (max 3 attempts, 30s apart). Or accept the empty score as "unknown" and re-score later. |
| **Lift Wing has no prediction cache.** Unlike ORES (which pre-computed scores), every call to Lift Wing runs a fresh inference. | All models | Scoring the same revision twice wastes rate limit quota and doubles latency. Risk: rate limit exhaustion in batch operations. | Implement your own cache (see [SOP: Implementing Caching](#sop-implementing-caching-lift-wing-has-none)). Use an LRU cache with TTL for short-lived tools, Redis/SQLite for persistent services. |
| **Revscoring models are frozen — no retraining.** The `{wiki}-goodfaith`, `{wiki}-damaging`, `{wiki}-articlequality` etc. models are ported from ORES with no further updates. | All Revscoring models (`{wiki}-*`) | Model accuracy degrades over time (model drift). No new language coverage. | Migrate to modern models (`revertrisk-*`, `articlequality` continuous, `outlink-topic-model`) where available. Check the [model list](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing#Revscoring_models_(migrated_from_ORES)) for coverage. |
| **Not all models cover all wikis.** Revscoring models only work on the wikis they were trained on. | `{wiki}-*` Revscoring models | Calling a model with an unsupported wiki returns a 404 or empty prediction. | Check the [model list](https://wikitech.wikimedia.org/wiki/Machine_Learning/LiftWing#Revscoring_models_(migrated_from_ORES)) before using a wiki-specific model. Use language-agnostic modern models for broader coverage. |
| **Per-second rate limits are tighter than per-hour.** Anonymous: 15 req/s. Authenticated: 100 req/s. Batch operations can hit the per-second limit before the per-hour limit. | All models | 429 errors during batch scoring. | Add `time.sleep(0.1)` between requests (10 req/s). Use `requests.Session()` for connection reuse. Implement exponential backoff on 429. Authenticate via OAuth for 100 req/s. |
| **Some models are internal-only.** Models in the "experimental" Kubernetes namespace are not accessible from external clients. | Varies | 403/404 errors when calling models not in the public API. | Only use models listed in the [public API reference](https://api.wikimedia.org/wiki/Lift_Wing_API/Reference). |
| **`article-descriptions` model is heavy and slow.** Generating a short description is computationally expensive due to the beam search pattern. | `article-descriptions` | Requests can take 10-30 seconds or timeout. | Use `num_beams=1` for faster (but lower quality) results. Increase HTTP timeout to 60s. |
---
## Tooling
### 🔧 CLI Scripts
| Script | Purpose | Usage Example |
|--------|---------|---------------|
| [`scripts/score-revision.sh`](./scripts/score-revision.sh) | Score a single revision or page via curl | `./score-revision.sh enwiki 123456789 revertrisk-language-agnostic en` |
| [`scripts/batch-score.sh`](./scripts/batch-score.sh) | Batch score multiple revisions from stdin or a file | `cat revids.txt \| ./batch-score.sh enwiki revertrisk-multilingual --output csv` |
| [`scripts/playground.sh`](./scripts/playground.sh) | **Interactive menu** — pick a model, enter input, see results. No arguments to memorize | `bash scripts/playground.sh` |
### 🐍 Python Assets
| Script | Purpose | Usage Example |
|--------|---------|---------------|
| [`assets/liftwing_multi_model.py`](./assets/liftwing_multi_model.py) | Score a revision against multiple models in parallel with caching | `python3 liftwing_multi_model.py enwiki 123456789 --all` |
| [`assets/article_quality_report.py`](./assets/article_quality_report.py) | Full article report: quality + readability + topics + reference need | `python3 article_quality_report.py Albert_Einstein en` |
| [`assets/patrol_simulator.py`](./assets/patrol_simulator.py) | **Simulate patrolling** — score the latest edit to a page with revert-risk + goodfaith + damaging, get a verdict | `python3 patrol_simulator.py Albert_Einstein` |
| [`assets/language_explorer.py`](./assets/language_explorer.py) | **Language explorer** — type text and see what language langid detects, with confidence | `python3 language_explorer.py --samples` (built-in test suite in 10 languages) |
### 📚 Reference Docs
| Document | Contents |
|----------|----------|
| [`references/model-schemas.md`](./references/model-schemas.md) | Complete request/response schema for all 15+ externally accessible models, available wikis per model, and rate limit tables |
> 💡 The code examples in the SOPs above are self-contained and ready to copy-paste.
> The scripts and assets add CLI convenience, batch processing, and production patterns.