Build model titles only from each provider's known words

Anyone can commit under an agent email, so a free-form Git name such as
"Private Person <noreply@anthropic.com>" became a published model title,
which the apps would ship in their About windows. Each provider now lists
the words a model title may use. A name with any other word credits the
agent's default model and repeats none of it; versions are bounded too.

Across the co-author trailers in the 13 apps' history, 18,099 keep their
model row and 7 unusual names now fall back to the default. Also pin that
the PR lookup reads the merge commit rather than the PR head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FredAppsandClaude Opus 5.5 committed 2026-11-04 13:49:44 +01:00
1 parent c2e0e404ab
commit e09a018c65
3 files changed
+87 -37

No files matched your search

+4 -2
View File
@@ -157,8 +157,10 @@ Devin author commits themselves; both are credited in the snapshot's `models`, w
under **About ▸ Models**. Agents are recognized only by their exact Git email or GitHub account
(`AGENT_EMAILS` and `AGENT_LOGINS` in the generator), never by name, so a person's work address at
an AI company is still a person. A recognized agent's model and version are read from its Git name
(`Claude Opus 5.5`, `GPT-6.1 Sol`, `DeepSeek V4.1 Flash`); unrecognized emails are skipped. Add a new
agent to those tables. Other `[bot]` accounts, such as dependabot, are left out.
(`Claude Opus 5.5`, `GPT-6.1 Sol`, `DeepSeek V4.1 Flash`), using only the words those tables list for
that provider: Git names are free text that ends up in the apps, so any other word credits the
agent's default model instead. Unrecognized emails are skipped. Add a new agent or model word to
those tables. Other `[bot]` accounts, such as dependabot, are left out.
Name preferences are verified against the commits that introduced their values. A Git
email-linked author is trusted for consent only when GitHub reports a valid signature whose signer
@@ -17,36 +17,38 @@ import tomllib
# AI agents identified by their exact Git email or GitHub account: (company, id prefix, default
# model, read the model and version from the Git name). Agents are credited as models, never people.
# model, the words a Git name may use to name a model). Agents are credited as models, never people.
# Git names are free text that ends up in the apps, so a model title holds only these words.
ANTHROPIC = ("Anthropic", "anthropic", "Claude", ("Claude", "Opus", "Sonnet", "Haiku", "Fable", "Mythos", "Code"))
OPENAI = ("OpenAI", "openai", "Codex", ("Codex", "ChatGPT", "GPT", "Sol", "Luna", "Astra", "Terra"))
ZHIPU = ("Zhipu AI", "zhipu", "GLM", ("GLM", "Flash", "ZCode"))
AGENT_EMAILS = {
"noreply@anthropic.com": ("Anthropic", "anthropic", "Claude", True),
"codex@openai.com": ("OpenAI", "openai", "Codex", True),
"noreply@openai.com": ("OpenAI", "openai", "Codex", True),
"cursoragent@cursor.com": ("Anysphere", "anysphere", "Cursor", False),
"noreply@deepseek.com": ("DeepSeek", "deepseek", "DeepSeek", True),
"noreply@z.ai": ("Zhipu AI", "zhipu", "GLM", True),
"noreply@zcode.ai": ("Zhipu AI", "zhipu", "GLM", True),
"noreply@qwen.ai": ("Alibaba", "alibaba", "Qwen", True),
"noreply@google.com": ("Google", "google", "Gemini", True),
"noreply@minimax.io": ("MiniMax", "minimax", "MiniMax", True),
"noreply@x.ai": ("xAI", "xai", "Grok", True),
"noreply@xiaomi.com": ("Xiaomi", "xiaomi", "MiMo", False),
"noreply@opencode.ai": ("OpenCode", "opencode", "OpenCode", True),
"noreply@codebuff.com": ("Codebuff", "codebuff", "Codebuff", False),
"noreply@paperclip.ing": ("Paperclip", "paperclip", "Paperclip", False),
"noreply@anthropic.com": ANTHROPIC,
"codex@openai.com": OPENAI,
"noreply@openai.com": OPENAI,
"cursoragent@cursor.com": ("Anysphere", "anysphere", "Cursor", ()),
"noreply@deepseek.com": ("DeepSeek", "deepseek", "DeepSeek", ("DeepSeek", "Flash", "Free")),
"noreply@z.ai": ZHIPU,
"noreply@zcode.ai": ZHIPU,
"noreply@qwen.ai": ("Alibaba", "alibaba", "Qwen", ("Qwen", "Flash")),
"noreply@google.com": ("Google", "google", "Gemini", ("Gemini", "Flash", "Antigravity")),
"noreply@minimax.io": ("MiniMax", "minimax", "MiniMax", ("MiniMax", "M3")),
"noreply@x.ai": ("xAI", "xai", "Grok", ("Grok",)),
"noreply@xiaomi.com": ("Xiaomi", "xiaomi", "MiMo", ()),
"noreply@opencode.ai": ("OpenCode", "opencode", "OpenCode", ("OpenCode", "Big", "Pickle")),
"noreply@codebuff.com": ("Codebuff", "codebuff", "Codebuff", ()),
"noreply@paperclip.ing": ("Paperclip", "paperclip", "Paperclip", ()),
}
AGENT_LOGINS = {
"claude": AGENT_EMAILS["noreply@anthropic.com"],
"codex": AGENT_EMAILS["codex@openai.com"],
"cursoragent": AGENT_EMAILS["cursoragent@cursor.com"],
"copilot": ("GitHub", "github", "Copilot", False),
"copilot-swe-agent[bot]": ("GitHub", "github", "Copilot", False),
"devin-ai-integration[bot]": ("Cognition", "cognition", "Devin", False),
"coderabbitai[bot]": ("CodeRabbit", "coderabbit", "CodeRabbit", False),
"capy-ai[bot]": ("Capy", "capy", "Capy", False),
"copilot": ("GitHub", "github", "Copilot", ()),
"copilot-swe-agent[bot]": ("GitHub", "github", "Copilot", ()),
"devin-ai-integration[bot]": ("Cognition", "cognition", "Devin", ()),
"coderabbitai[bot]": ("CodeRabbit", "coderabbit", "CodeRabbit", ()),
"capy-ai[bot]": ("Capy", "capy", "Capy", ()),
}
# Display casing for model words that Git names often write in lower case.
MODEL_WORDS = {word.casefold(): word for word in ("GPT", "ChatGPT", "GLM", "DeepSeek", "MiMo", "OpenCode", "MiniMax")}
def agent(email, login=None):
@@ -253,30 +255,33 @@ def model(trailer):
known = agent(email)
if known is None:
return None
company, prefix, default, named = known
company, prefix, default, allowed = known
spelling = {word.casefold(): word for word in allowed}
def parse(text):
"""Split "GPT-6.1 Sol" into its model words and first version number."""
"""Split "GPT-6.1 Sol" into model words and a version, or None if any word is unknown."""
words = [word for word in re.split(r"[\s_-]+", text) if word]
# Drop a bare company prefix ("OpenAI Codex"), not a brand that names the model ("DeepSeek V3.2").
if words and words[0].casefold() == company.casefold() != default.split()[0].casefold():
words = words[1:]
family, version = [], ""
for word in words:
if not version and re.fullmatch(r"[vV]?\d+(?:\.\d+)*", word):
if not version and re.fullmatch(r"[vV]?\d{1,3}(?:\.\d{1,3}){0,2}", word):
version = word.lstrip("vV")
elif word.casefold() in spelling:
family.append(spelling[word.casefold()])
else:
family.append(MODEL_WORDS.get(word.casefold(), word[:1].upper() + word[1:]))
return None
return family, version
family, version = [], ""
if named:
# "Claude Opus 5.5 (1M context)", "GPT-6.1 Sol", "OpenAI Codex", "DeepSeek V4.1 Flash";
# "Codex (GPT-6)" names its model in brackets when the outer name has no version.
family, version = parse(re.sub(r"\([^)]*\)", " ", name))
for inner in re.findall(r"\(([^)]*)\)", name):
if not version and parse(inner)[1]:
family, version = parse(inner)
# "Claude Opus 5.5 (1M context)", "GPT-6.1 Sol", "OpenAI Codex", "DeepSeek V4.1 Flash"; "Codex
# (GPT-6)" names its model in brackets when the outer name has no version. A name with any other
# word, such as a person's, credits the agent's default model and repeats none of it.
family, version = (parse(re.sub(r"\([^)]*\)", " ", name)) if allowed else None) or ([], "")
for inner in re.findall(r"\(([^)]*)\)", name) if allowed else []:
found = parse(inner)
if not version and found and found[1]:
family, version = found
title = " ".join(family) or default
return dict(id=f"{prefix}/" + title.casefold().replace(" ", "-") + (f"/{version}" if version else ""),
company=company, model=title, version=version)
+43
View File
@@ -3,6 +3,7 @@ import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
@@ -386,6 +387,34 @@ class ContributorRefreshTests(unittest.TestCase):
with self.subTest(trailer=trailer):
self.assertIsNone(credits.model(trailer))
def test_free_text_git_names_never_become_model_titles(self):
"""Anyone can commit under an agent email; only known words reach the apps."""
claude, codex = ("anthropic/claude", "Claude", ""), ("openai/codex", "Codex", "")
for trailer, expected in {
"Private Person <noreply@anthropic.com>": claude,
"Claude Buy This 5.5 <noreply@anthropic.com>": claude,
"Claude 123456789 <noreply@anthropic.com>": claude,
"Mallory <codex@openai.com>": codex,
"GPT Mallory 6 <noreply@openai.com>": codex,
"Mallory (nothing here) <noreply@openai.com>": codex,
"Mallory (GPT-6) <noreply@openai.com>": ("openai/gpt/6", "GPT", "6"),
"Mallory <noreply@deepseek.com>": ("deepseek/deepseek", "DeepSeek", ""),
}.items():
with self.subTest(trailer=trailer):
identity = credits.model(trailer)
self.assertEqual((identity["id"], identity["model"], identity["version"]), expected)
def test_a_person_committing_under_an_agent_email_is_not_published(self):
"""The commit credits the agent's default model and repeats nothing of the Git name."""
(self.root / "text.txt").write_text("spoofed\n")
self.commit("Spoofed author", None, author=("Private Person", "noreply@anthropic.com"))
(self.root / "text.txt").write_text("spoofed trailer\n")
self.commit("Spoofed trailer\n\nCo-authored-by: Other Person <noreply@openai.com>", None)
result = self.collect()
self.assertEqual({row["id"]: row["commits"] for row in result["models"]}, {"anthropic/claude": 1, "openai/codex": 1})
self.assertNotIn("Private Person", json.dumps(result))
self.assertNotIn("Other Person", json.dumps(result))
def test_profiles_are_requested_only_for_verified_opt_in_entries(self):
"""A copied people entry never enables name synchronization."""
result = self.collect()
@@ -430,9 +459,23 @@ class ContributorRefreshTests(unittest.TestCase):
pull_queries = [query for endpoint, query, _ in self.api_calls if "/pulls?" in endpoint]
self.assertEqual(len(pull_queries), 1)
self.assertIn("select(.merged_at != null)", pull_queries[0])
self.assertIn("sha: .merge_commit_sha", pull_queries[0])
self.assertEqual(len(result["contributors"]), 1)
self.assertEqual(result["contributors"][0]["prs"], 1)
@unittest.skipUnless(shutil.which("jq"), "needs jq, the language of gh's --jq")
def test_pull_projection_selects_the_merge_commit_not_the_head(self):
"""A squash merge's head commit is not on the base branch; only the merge commit is."""
self.collect()
query = next(query for endpoint, query, _ in self.api_calls if "/pulls?" in endpoint)
pulls = [dict(number=7, merged_at="2026-11-01T00:00:00Z", merge_commit_sha="a" * 40,
head=dict(sha="b" * 40), user=dict(login="alice")),
dict(number=8, merged_at=None, merge_commit_sha="c" * 40,
head=dict(sha="d" * 40), user=dict(login="bob"))]
output = subprocess.check_output(["jq", "-c", query], input=json.dumps(pulls), text=True)
self.assertEqual([json.loads(line) for line in output.splitlines()],
[dict(number=7, sha="a" * 40, login="alice")])
def test_unknown_identity_is_not_guessed_from_git_author(self):
"""Keep unmapped commits visible as unattributed statistics."""
(self.root / "text.txt").write_text("unmapped\n")