mirror of
https://github.com/storytold/craft-common-actions.git
synced 2026-11-13 22:12:24 +00:00
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:
1 parent
c2e0e404ab
commit
e09a018c65
3 files changed
+87
-37
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user