cleanyourtv-app
Clean Your TV를 위한 커뮤니티 제작 모바일 앱
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Switch AI models without breaking your product: find every model your code calls and when it retires, turn real usage logs into tests, compare and pick replacement models. CLI, GitHub Action and MCP server.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Switch AI models without breaking your product.
GPT-4o, GPT-4o mini and GPT-4.1 retire on 14 January 2027, GPT-5 and o3 on 11 December, Claude Sonnet 4.5 on 30 November. Does your code call one of them? Find out in seconds, with nothing to install:
uvx llmshift scan # or: pipx run llmshift scan
A real run: scan on a sample repository, then compare replaying llmshift's demo traffic on Claude Haiku 5.5.
Leaving the OpenAI Evals platform? It goes read-only on 31 October and its API shuts down on 30 November 2026. llmshift import openai-evals --api --all pulls every eval, its latest run and the graded outputs into tests you keep, and runs them on any model (how, or the step-by-step guide).
Then test the replacement on your real traffic before the shutdown date: llmshift snapshot turns your logs into tests and llmshift compare --to <replacement> shows what changes. llmshift demo walks through it with simulated models, no API key needed.
Providers now retire models roughly every six months. When a model's behaviour changes, outputs can look fine and still be wrong, and nothing raises an error. LLMShift is built around the moment of switching models, from finding the calls to proving the new model works:
| Command | What it does |
|---|---|
| Find what has to move | |
llmshift scan | Finds every model your code calls (Python, JS/TS, Go, Java, configs, .env, notebooks) and warns about retirement dates. |
llmshift digest | One message (Slack, Teams, e-mail, ...) listing the models your repositories use that are retired or retire soon. |
| Turn real traffic into tests | |
llmshift snapshot | Turns real usage logs, including multi-step agent runs, into a test set automatically. You write no tests. |
llmshift import openai-evals | Pulls every eval, its latest run and the graded outputs out of the OpenAI Evals API (--api --all) into tests you keep. The Evals platform goes read-only on 31 Oct 2026 and its API shuts down on 30 Nov 2026. |
| Choose the new model | |
llmshift compare | Runs the tests on the new model and shows exactly what changed: broken formats, wrong facts, different tone, different tool calls. |
llmshift pick | Tries several cheaper models at once and recommends the cheapest one that still passes, with the monthly saving. |
llmshift safety | Does the new model refuse more, follow injected instructions or leak private data? |
llmshift embeddings | Shows how search results change before you switch embedding models. |
| Switch | |
llmshift fix | Suggests prompt changes until the new model matches the old one, and points to where each prompt lives in your code. |
llmshift upgrade | Swaps a retiring model in your code and opens a pull request with the test results ("Dependabot for AI models"). |
llmshift check | On every pull request, tests only the prompts it edits, and comments with the result. |
| After switching | |
llmshift shadow report | Checks a new model on a share of live traffic, copied in the background (shadow mode). |
AI coding assistants can use all of this as tools: llmshift mcp is an MCP server for Claude Code, Cursor and VS Code.
LLMShift is a single pure-Python package with no compiled code: its only dependency is tomli on Python 3.9 and 3.10 (to read pyproject.toml), and a copy of PyYAML's pure-Python YAML parser is included. It talks to OpenAI, Azure OpenAI, Anthropic, Gemini and any OpenAI-compatible endpoint over plain HTTPS, so there are no SDK version conflicts. You can also run it as a GitHub Action.
pip install llmshift
llmshift demo # 1-minute offline walkthrough with simulated models (no API key)
It runs on Python 3.9 to 3.15 on Linux, macOS and Windows, and installs without a compiler on any of them.
# 1. Which models does the code use, and when do they retire?
llmshift scan
# 2. Turn real traffic into tests (OpenAI/Anthropic/Gemini logs, LangSmith/Langfuse exports, CSV, ...)
llmshift snapshot logs/ --model "gpt-4o*" --since 2026-09-01
# 3. What changes on the new model? Or: which cheaper model would do?
export OPENAI_API_KEY=...
llmshift compare --to openai:gpt-5.6-sol
llmshift pick --models openai:gpt-6-luna,anthropic:claude-haiku-5-5 --volume 100k
# 4. Get prompt changes that close the gaps, then open the upgrade pull request
llmshift fix --to openai:gpt-5.6-sol
llmshift upgrade --from gpt-4o-2024-05-13 --apply --compare --pr
# 5. After switching, watch the live model every night
llmshift watch --model openai:gpt-5.6-sol --alert slack:$SLACK_WEBHOOK_URL
llmshift init --github writes a llmshift.yaml and two workflows: PR checks and a nightly watch. More workflows are in examples/workflows/.
$ llmshift scan
LLMShift scan 5 files, 6 model references, 4 distinct models (lifecycle data: 2026-10-21)
✗ RETIRED claude-3-5-haiku-20241022 anthropic
retired on 2026-02-19; replacement: claude-haiku-4-5-20251001
app/summaries.py:10 model="claude-3-5-haiku-20241022",
! 2 DAYS LEFT gpt-4o-2024-05-13 openai
retires 2026-10-23 (2 days left); replacement: gpt-5.6-sol
app/triage.py:7 TRIAGE_MODEL = "gpt-4o-2024-05-13"
app/triage.py:17 model=TRIAGE_MODEL, (via TRIAGE_MODEL, line 7)
app/replies.py:15 model=os.getenv("REPLY_MODEL", "gpt-4o-2024-05-13"),
model=-style arguments and models passed to known constructors (GenerativeModel, init_chat_model, pydantic-ai Agent, LiteLLM, ...). It also handles {"model": ...} dicts, constants used elsewhere (MODEL = "..." then model=MODEL) and os.getenv("X", "default") fallbacks. Docstrings and comments are ignored..env, INI, Terraform and Dockerfiles. Notebooks: code cells, reported with cell numbers.us.anthropic.claude-...-v1:0, full ARNs), Vertex (claude-...@20241022, projects/.../models/...), OpenRouter/LiteLLM prefixes (openai/gpt-4o), dotted spellings (claude-3.5-sonnet) and fine-tunes (ft:gpt-4o-mini-..., which follow their base model's schedule) all map to their base model..llmshift/ folder. --exclude takes globs: a bare name (legacy) matches at any depth, and paths may be relative to the scanned directory or the current one.llmshift update downloads newer data (below); add or override entries with --registry extra.json; llmshift models gpt-4o searches the data.--fail-on retired|retiring|deprecated|unknown|never (default retired). Output formats: --format table|json|markdown|sarif. Inside GitHub Actions, findings appear as annotations on the diff and in the job summary.llmshift snapshot logs/ -o .llmshift/tests.jsonl
snapshot reads JSONL, JSON and CSV files (optionally .gz) and detects the format of each record:
custom_id)generateContent request/response pairs, including logged streams (server-sent events are reassembled){"request": {"body": ...}} records and flat logs{"messages": [...]}, ShareGPT) and Langfuse / LangSmith-style exportsprompt / input / question and output / response / answer columnsEmbedding, moderation, image and audio calls are skipped, and every skipped record is counted with its reason. --append adds only cases that are not already in the test file. Logs of any size are read as a stream: beyond --max-records calls (default 100,000) a fair sample is kept for every prompt template.
Then it:
--model, --since and --until filters. Each case keeps the request's settings, extended thinking included: OpenAI's reasoning effort, Claude's thinking and effort, and Gemini's thinkingConfig.--redact-pii it also replaces e-mails, phone numbers and card numbers, using consistent placeholders so echoed values still match.--max-cases, --per-group).| Output looks like | Checks recorded |
|---|---|
| JSON | valid JSON (no code fences), JSON schema (required keys are those present in every output of the template), field values for labels, flags, IDs and amounts |
| a label / number | exact value, plus the label vocabulary seen across the template |
| prose / markdown | facts (numbers with units and scale, dates, IDs, URLs, names), language, markdown structure, length band, tone, similarity |
| tool calls | same tools called, argument keys, short argument values |
| an agent run | the steps: tool calls, loops, step count (below) |
| any | not empty, not a refusal |
Logs without outputs? Pass --baseline-model openai:gpt-4o-2024-08-06 to generate them.
No logs yet? Record them from your app with one line:
from llmshift.record import wrap
client = wrap(OpenAI(), path="logs/llm.jsonl") # works with OpenAI, AsyncOpenAI, Anthropic, AsyncAnthropic, streaming
Streamed responses, including the SDKs' stream helpers (with client.messages.stream(...), client.chat.completions.stream(...)), are logged whole once the stream ends, with tool calls and token usage. Calls through with_raw_response and with_streaming_response are logged when your code parses the answer (.parse()); helpers that loop over the API inside the SDK, such as Anthropic's tool_runner, call the unwrapped client and are not logged. Pydantic response formats (parse(response_format=Model) and friends) are logged as the JSON schema the SDK sends, so the cases replay with the same structured output. Several threads or processes can log to the same file. To other libraries the wrapped client looks like the SDK's own: instructor works on it, and a tracer that patches create afterwards (LangSmith's wrap_openai) still gets each call logged once.
The test file is plain JSON Lines that you can read, edit and commit:
{"id": "c_bc05e87da4", "group": "acme-customer-support-assistant-8a772a",
"input": {"messages": [{"role": "system", "content": "You are Acme's customer support assistant..."},
{"role": "user", "content": "Hi, my order #4521 hasn't arrived yet..."}], "params": {"temperature": 0.3}},
"baseline": {"output": "Thank you for contacting Acme support. ...", "model": "gpt-4o-2024-05-13"},
"checks": [{"type": "facts", "facts": [{"kind": "id", "text": "#4521"}, {"kind": "date", "text": "March 14", "value": "--03-14"}]},
{"type": "tone"}, {"type": "length", "min_words": 15, "max_words": 95, "baseline_words": 40}]}
You can also write cases by hand in YAML (prompt:, system:, baseline:, checks:).
Run each task inside agent_run so its model calls and tool results are recorded as one run:
from llmshift.record import agent_run, wrap
client = wrap(OpenAI(), path="logs/agent.jsonl")
with agent_run("refund-flow"):
run_my_agent(client, task)
Logs without run ids are linked too, when each request continues the previous one with its tool results. snapshot turns each run into one test case. compare replays it on the new model: the model starts from the run's first request, and whenever it calls a tool it gets the result recorded for that call. Both models see the same world and no real tool runs. The agent check reports missing, extra or changed tool calls, tools called in a different order, loops (the same call 3 or more times, more often than recorded) and runs that hit the step limit; the usual checks compare the final answer. Reports show both step traces and the cost per task (here with simulated models):
✗ FAIL c_ef9ea0976f acme-support-agent-look-dfa329-agent
Input: What is happening with order #9001?
Steps (old): lookup_order(order_id=9001) → answer
Steps (new): lookup_order(order_id=9001) → lookup_order(order_id=9001) → lookup_order(order_…
✗ tools looped: called lookup_order(order_id=9001) 6 times; stopped after 6 steps w…
Notes
• Agent tasks (3): 2 -> 4 steps on average; cost per task $0.00035 -> $0.00062 (+76%); 1 task(s) looped; 1 ran out of steps.
A call that was never recorded gets the recorded result of the same tool with other arguments if there is one, else a placeholder saying nothing was recorded; the report counts these. The step limit is twice the recorded steps plus two (at least 6); set "max_steps" in a case's agent object to change it.
$ llmshift compare --to openai:gpt-5.6-sol
✓ 0 unchanged ! 6 changed ✗ 12 failed
pass rate ███████░░░░░░░░░░░░░ 33.3% ✗ below the 95% threshold
95% range 10.9%-56.2% over 18 cases
What changed
Broken format 8 failed
JSON is wrapped in a markdown code fence (8)
Wrong or missing facts 6 failed, 6 changed
facts missing (6), e.g. no mention of "Acme"
numbers changed (4), e.g. "12.5%" -> "12%"; "$4.2 million" -> "$4 million"; "3.1%" -> "3%"
$.urgency changed (2), e.g. $.urgency: "medium" -> "high"
Different tone / style 6 changed
adds emoji (6) · more exclamatory (6) · more casual (6) · less polite (6)
✗ FAIL c_0d9f963af7 support-ticket-triage-assistant-f98626
Input: The tracking link for order #8812 doesn't work.
✗ format JSON is wrapped in a markdown code fence (```json ... ```)
Old: {"category": "shipping", "urgency": "low", "summary": "The tracking link for order #8812 doesn't work."}
New: ```json
{ "category": "shipping", ... }
```
--severity tone=error. A case whose check could not run (an invalid regex, one stopped after 2 seconds, a judge that cannot be reached) is not checked: it is reported apart with the reason, left out of the pass rate, and the run exits with code 2 instead of blaming the new model.--baseline-model <old model> also runs the old model live. A check that the old model fails as well is counted as noise, not a regression, and the two models are compared case by case (McNemar), so "worse" means significantly worse.--judge openai:gpt-5.6-terra adds an LLM equivalence judge to free-text answers (JSON, labels and numbers are already checked exactly), and grades imported model graders. judge: in llmshift.yaml does the same for every run..llmshift/reports/. Add --markdown summary.md for PR comments and --junit junit.xml for CI test views; without a file name they are written to .llmshift/reports/ too. llmshift report results.json --html re-renders a saved run.temperature other than 1, or any top_p or top_k, is a 400 error) and only adaptive thinking (a budget_tokens is a 400 error); with thinking on, every Claude model takes only temperature 1, no top_k and a top_p of 0.95 or more; and OpenAI's reasoning models take only temperature 1. Thinking and effort are sent in the form each model takes (a budget, adaptive, Sonnet 5.5's between_tools), and the report says what to send instead. The answer is still tested without the setting, but the case fails, because the real call would. Once your code no longer sends it, test with --drop-param temperature (or drop_params: [temperature] in llmshift.yaml); llmshift upgrade lists where the code sets it. If a proxy between your code and the model removes such settings for you (LiteLLM's drop_params), set severity: {request: info} to keep the note without failing the case.max_tokens vs , Responses-only models, and the role. You'll also see notes such as "reasoning models count thinking tokens toward the output limit".--repeat N runs each case N times (each run cached separately). A case's result is its most common outcome, the worse one on a tie, and cases that pass in some runs but not others are marked flaky in every report, so one unlucky sample doesn't decide a switch. The pass rate becomes the share of passing runs.--judge, cases that already failed a rule-based check, and answers identical or near-identical to the old one (ROUGE-L ≥ 0.9), are settled without a judge call. The report counts the calls saved; --judge-mode always asks every time.llmshift judge label walks you through about 20 old/new answer pairs from the last compare run and saves each answer at once. llmshift judge calibrate asks the judge about the same pairs and reports how often it agrees with you (with Cohen's kappa), and whether it errs on the lenient or the strict side. Every later compare with that judge quotes the agreement.llmshift pick --models openai:gpt-6-luna,anthropic:claude-haiku-5-5,gemini:gemini-3.8-flash --volume 100k
pick runs your real-traffic tests on several candidate models at once, with the same checks as compare, and recommends the cheapest one that keeps quality. From the offline demo:
LLMShift pick 18 test case(s), 2 candidate(s), quality bar 95%
Today: mock:legacy $0.78 per 1,000 requests (from your recorded usage)
Model Pass rate 95% range Cost / 1k req vs today p50 latency
─ ──────────── ──────────── ───────── ───────────── ──────── ───────────
✓ mock:frugal 100% (18/18) 82%-100% $0.028 -96% 0 ms
✗ mock:nextgen 33% (6/18) 11%-56% $0.37 -53% 0 ms
Switch to mock:frugal: same quality (all 18 cases unchanged), 96% cheaper - about $75.22 a month saved at 100,000 requests a month.
~). Thinking tokens cost what output does, cached input and 1-hour cache writes their own prices, and a long prompt its long-context price where the list has one. A candidate with cases that could not be checked is not recommended. Prices come from a bundled snapshot of LiteLLM's price list (llmshift update --prices refreshes it); set your own in USD per 1M tokens with --price azure:prod=2.5/10 or under pricing: in llmshift.yaml.--baseline (default: baseline in llmshift.yaml, else the recorded model). When today's model is also one of the candidates, today's cost is measured on the same tests instead, so every saving compares like with like.--min-pass-rate (default min_pass_rate, 0.95). If nothing passes, pick names the closest candidate and what broke; llmshift fix --to <model> can often close the gap.--format json|markdown, --json [FILE], --markdown [FILE]. Inside GitHub Actions the result goes to the job summary and the action's recommended output. Exit code 0 when a model is recommended, 1 when none passes, and 2 when some cases could not be checked and the rest pass (no verdict yet).llmshift safety --to openai:gpt-5.6-sol --baseline-model openai:gpt-4o-2024-08-06
Both models get the same probes, built from your own test cases:
Probes: Refusals 28 (18 of your requests + 10 harmless questions with alarming words), Prompt injection 20, PII leakage 20; seed 7. ...
Category Probes Old New Change p Verdict
──────────────── ────── ─── ──── ──────── ────── ───────
Refusals 28 0% 36% +36 pts <0.001 RISKIER
Prompt injection 20 0% 100% +100 pts <0.001 RISKIER
PII leakage 20 0% 100% +100 pts <0.001 RISKIER
A category counts as riskier only when it is more than --max-increase worse (default 5 points) and the paired difference is significant (McNemar). The exit code is then 1, so CI can block the switch. Use --probes N per category (default 20; refusals use each test case at most once and add N/2, at most 10, harmless questions with alarming words, so the demo's 18 cases give 28), --categories injection,pii, and --format json|markdown (also --json / --markdown files).
llmshift embeddings --from openai:text-embedding-3-small --to openai:text-embedding-3-large --corpus docs/ --queries-from logs/
Your documents (.txt .md .rst .html .json .jsonl) are chunked and a sample is embedded with both models. For each real query (from --queries FILE, or from the embedding requests in your logs) the top-k results are compared: how many survive, whether the old top result is still found, and rank-biased overlap. You also get the cost and time of re-embedding the whole corpus with the new model, and a warning when the vector size changes (the index schema must change). Exits 1 when fewer than --min-overlap (default 60%) of results are kept.
$ llmshift fix --to openai:gpt-5.6-sol
support-ticket-triage-assistant-f98626 (prompt at app/triage.py:10)
before: 0 of 7 match (7 failing) after: 5 of 7 match (2 failing) via rules
Add to the system prompt:
+ Return only the raw JSON object. Do not wrap it in markdown code fences (```) and do not add any text before or after it.
+ For "urgency", use exactly one of: low, medium, high.
still different: facts: $.urgency: "medium" -> "high"
Overall pass rate: 33% → 89% with the suggested prompts
For each prompt template with regressions, LLMShift builds candidate prompts and tests each one on the new model, keeping the best. The candidates come from:
--optimizer)Your code is never changed. You get the new prompt, a diff, and where the prompt lives in your repository. --write-tests fixed.jsonl writes a patched test set so you can re-check it with compare.
llmshift upgrade --from gpt-4o-2024-05-13 # dry run: shows the diff
llmshift upgrade --auto --apply --compare --pr # every retiring model with a replacement
LLMShift upgrade
gpt-4o-2024-05-13 -> gpt-5.6-sol (retiring on 2026-10-23): 2 place(s)
note: app/replies.py:16: gpt-5.6-sol may only accept the default temperature; check the temperature setting
note: app/triage.py:18: gpt-5.6-sol may only accept the default temperature; check the temperature setting
--- a/app/triage.py
+++ b/app/triage.py
@@ -4,7 +4,7 @@
from openai import OpenAI
client = OpenAI()
-TRIAGE_MODEL = "gpt-4o-2024-05-13"
+TRIAGE_MODEL = "gpt-5.6-sol"
TRIAGE_PROMPT = (
"You are a support ticket triage assistant for Acme. Classify the customer's message and return JSON with the keys "
...
Dry run: nothing was changed. Re-run with --apply to write these edits, or --pr to open a pull request.
upgrade swaps the model id exactly where scan found it. It handles constants, os.getenv defaults, configs and notebooks, and keeps provider prefixes such as openai/. For OpenAI's o-series and GPT-5+ it renames max_tokens to max_completion_tokens in the calls that use the swapped model. It flags settings to check by hand, such as temperature and fine-tuned or Bedrock ids; moving to Claude 4.7 or later, it lists every temperature, top_p and top_k still set in code or settings that talk to Claude, since those calls would fail. Everything else in each file stays byte-for-byte the same (encoding, BOM, line endings). --apply first saves a file git cannot give back as it is now (no repository, untracked, uncommitted changes) as NAME.bak (--no-backup skips that), and stops before writing anything if a file was edited after the diff was made. --fix appends llmshift fix suggestions to prompts written as plain literals. --compare tests the replacement on your recorded traffic. --pr pushes a llmshift/upgrade-... branch and opens, or updates, a pull request with the diff and results. See examples/workflows/llmshift-upgrade.yml for a weekly run. In GitHub Actions, turn on Settings > Actions > General > "Allow GitHub Actions to create and approve pull requests" once (new repositories have it off); checks on the pull request it opens then wait for your approval unless you pass a personal access token.
llmshift check # in CI: runs only the tests for prompts this change edits
llmshift check --model openai:gpt-5.6-sol --base origin/main --markdown
LLMShift check model openai:gpt-5.6-sol base origin/main
✗ support-ticket-triage-assistant-f98626 app/triage.py 8 case(s) pass rate 75% (below 95%)
facts: $.urgency changed (2)
check looks up each test group's recorded system prompt in the files the change touches: Python strings (including implicit concatenation and f-strings), string literals in other languages, YAML/JSON values and prompt files (.txt, .md, .prompt, .j2). When the base version held the prompt and the change edits it, that group's tests run with the edited text on the model in production (baseline in llmshift.yaml). The result shows whether the model still answers the way it did before the edit. It also notices prompts that moved or were removed. In a pull request it posts one comment and updates it on every push. Check out with fetch-depth: 0 so the base branch is available. See examples/workflows/llmshift-check.yml.
from llmshift.record import wrap
client = wrap(OpenAI(), shadow="openai:gpt-5.6-sol", shadow_rate=0.05) # add record=False to shadow only
After a real call returns, a share of the requests (here 5%) is copied to the new model on a background thread. The app never waits for the copy and never sees its errors. If the new model falls behind, copies are dropped rather than queued without limit, and a slow or hanging shadow model never delays the app's exit by more than 5 seconds. Each pair (the real answer and the shadow answer) is appended to logs/llmshift-shadow.jsonl with secrets redacted, and
llmshift shadow report
checks the shadow answers against the real ones with the same checks as compare (exit 1 below min_pass_rate; --html for the full report). For more control, pass a llmshift.shadow.Shadow("openai:gpt-5.6-sol", rate=0.05, max_workers=2, max_pending=100); its flush() waits for the copies in flight, which helps in short scripts.
llmshift watch --model openai:gpt-5.6-sol --alert slack:$SLACK_WEBHOOK_URL --alert github
llmshift watch --model openai:gpt-5.6-sol --at 02:00 # keep running, once a day
Each run calls the live model (no cache) and appends to .llmshift/watch/<model>/history.jsonl. It alerts on:
--min-pass-rate (or watch.min_pass_rate, then min_pass_rate, from llmshift.yaml; set it to null to rely on drift alone)When the test set changes, the next run becomes the new reference. Destinations: slack:<url>, teams:<url>, discord:<url>, webhook:<url>, github (opens or updates an issue), email:<addr> (SMTP) and file:<path>. slack, teams and discord alone read the URL from SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL or DISCORD_WEBHOOK_URL. --alert-on sets the lowest severity that is sent (default warning). The exit code is 1 when drift reaches --fail-on (default warning), so a scheduled CI job turns red.
llmshift update downloads the latest lifecycle data from this repository (--prices also refreshes model prices; --check only says whether newer data exists). The download is validated and kept in your user data folder (LLMSHIFT_HOME to move it), and is used only while it is newer than the data bundled with your version. llmshift doctor shows which data is in use.llmshift digest repo-a repo-b --alert slack sends one message listing the models your repositories use that are retired or retire within 30 days (--within DAYS). --dry-run prints it without sending. A weekly example is in examples/workflows/llmshift-digest.yml.llmshift calendar --out site builds a searchable retirement-calendar website with a page for every model and provider (shutdown date, replacement, source, the error a retired model returns and how to move off it), an iCalendar feed (retirements.ics) you can subscribe to, retirements.json, and llms.txt for AI assistants. --base-url https://you.github.io/repo/ adds canonical links, sitemap.xml, robots.txt and an RSS feed (feed.xml). Add --goatcounter <code> to count visits with GoatCounter, which sets no cookies. In this repository, the calendar workflow publishes it to GitHub Pages every day.registry-watch reads the deprecation tables of OpenAI, Anthropic, the Gemini API and Vertex AI, merges new shutdown dates, replacements and newly deprecated models (it never removes anything, and models that already retired keep their record), runs the tests and commits the result, which llmshift update then brings to you. Changes on the other source pages, or more than 40 at once, open an issue for a person to review.OpenAI's hosted Evals become read-only on 2026-10-31, and the dashboard and API shut down on 2026-11-30. The dashboard's exports do not cover every kind of eval, and a file of past results cannot be run again on its own, so the API is the complete way out:
export OPENAI_API_KEY=...
llmshift import openai-evals --api --all --split .llmshift/evals/ --save-raw
llmshift compare --tests .llmshift/evals/ --to openai:gpt-5.6-sol --judge openai:gpt-5.6-terra
--save-raw also keeps every API response exactly as read (the evals with their graders, every run with its data source and settings, the output items and the data files) in a dated file next to the tests, such as .llmshift/evals/openai-evals-raw-2026-10-29.json, or in the file you name. Commit it with the tests: after 30 November it is the only copy of what the original evals did, and the grader definitions are what you will compare against. It holds response bodies only, never your API key.string_check (eq/ne/like/ilike) → equals / not_equals / containstext_similarity → similarity, with the same metric and thresholdlabel_model → llm_label and score_model → llm_score, graded by --judge or the grader's own modelpython → python, which runs locally only with --allow-pythonmulti → multiimport does not replace an existing test file unless you pass --force (or choose another file with -o).o3-mini, o4-mini, gpt-4.1-nano and others shut down on 2026-10-23).llmshift import openai-evals eval.json --data items.jsonl) and the open-source openai/evals registry (llmshift import openai-evals path/to/evals/registry). The registry covers Match, Includes, FuzzyMatch, JsonMatch and ModelBasedClassify (fact, closedqa, custom specs).llmshift mcp runs a Model Context Protocol server, so an AI coding assistant can migrate a codebase itself, with LLMShift as the safety net: "move this repo off gpt-4o and prove nothing broke". It is listed in the official MCP Registry as io.github.DeChuks-hub/llmshift.
Claude Code, as a plugin with the server and a migrate-models skill that walks Claude through scan, snapshot, compare, fix and upgrade:
claude plugin marketplace add DeChuks-hub/llmshift
claude plugin install llmshift@llmshift
Or the server alone: claude mcp add llmshift -- uvx llmshift mcp.
Cursor, in .cursor/mcp.json:
{"mcpServers": {"llmshift": {"command": "uvx", "args": ["llmshift", "mcp"]}}}
VS Code, in .vscode/mcp.json:
{"servers": {"llmshift": {"type": "stdio", "command": "uvx", "args": ["llmshift", "mcp"]}}}
uvx (from uv) runs the server without installing anything first; with LLMShift installed, use "command": "llmshift", "args": ["mcp"].
The tools are llmshift_scan, llmshift_models, llmshift_snapshot, llmshift_compare, llmshift_fix, llmshift_pick, llmshift_safety, llmshift_check, llmshift_upgrade and llmshift_doctor. They work in the project the assistant has open (pass --cwd DIR to pin another), read llmshift.yaml like the CLI, and use the same API keys. Each tool tells the client whether it calls models or writes files, so assistants can ask first: llmshift_snapshot writes the test file, and llmshift_upgrade edits code only when called with apply. llmshift_fix never edits code; it returns suggested prompts and where they live.
A model is written provider:model; well-known IDs work without a prefix.
| Spec | Credentials |
|---|---|
openai:gpt-5.6-sol, gpt-6-astra | OPENAI_API_KEY (OPENAI_BASE_URL, OPENAI_ORG_ID, OPENAI_PROJECT_ID optional) |
azure:<deployment> | AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY (or AZURE_OPENAI_AD_TOKEN); api_version default 2024-10-21, or v1 |
anthropic:claude-sonnet-5-5 | ANTHROPIC_API_KEY |
gemini:gemini-3.8-flash | GEMINI_API_KEY or GOOGLE_API_KEY |
openrouter:, together:, groq:, mistral:, deepseek:, xai:, fireworks:, perplexity:, cohere: | <NAME>_API_KEY |
ollama:llama3.1:8b, lmstudio:..., vllm:... | local, no key (OLLAMA_HOST respected) |
python:my_app.llm:answer | your own function: test the real pipeline, not just the raw model (it runs as long as it takes, unless timeout: is set) |
<name>:model from providers: in llmshift.yaml | any OpenAI-compatible endpoint (LiteLLM proxy for Bedrock/Vertex, ...) or a command |
mock:legacy, mock:nextgen, mock:frugal, mock:echo | offline simulated models (demo/tests); mock:embed-v1, mock:embed-v2 for embeddings |
llmshift doctor shows which credentials it can see.
- uses: DeChuks-hub/llmshift@v0
with:
command: compare
args: --to openai:gpt-5.6-sol --min-pass-rate 0.95 --junit llmshift-junit.xml
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
@v0 follows the newest 0.x release: it moves once PyPI has a release, and never to a pre-release. Pin @v0.1.7 or a commit SHA for builds that must not change.
Inputs: command (any LLMShift command), args (split like shell words: quote an argument that holds spaces; nothing is expanded), python-version, version (empty = install from the action's source), working-directory. Outputs: pass-rate, retired, retiring, recommended (from pick). Complete workflows are in examples/workflows/: PR checks, a nightly watch, prompt checks on pull requests, weekly upgrade pull requests and a weekly retirement digest. Pull requests from forks and Dependabot get no secrets, so those workflows skip the comparison there with a notice.
llmshift scan --format sarif writes SARIF: upload it with github/codeql-action/upload-sarif and retiring models show up as code scanning alerts, on the lines that call them.
Stop a commit that calls a retired model, with pre-commit:
- repo: https://github.com/DeChuks-hub/llmshift
rev: v0.1.7
hooks:
- id: llmshift-scan # retired models
# - id: llmshift-scan-retiring # also models that retire within 180 days
The hooks run when code or config files change, and scan the whole repository.
Settings live in llmshift.yaml or under [tool.llmshift] in pyproject.toml. The nearest one in the current directory or a parent is used, up to the git root or your home directory, but never one from a drive root or one that another user owns (SECURITY.md). Relative paths in it are relative to the file, so commands work from any subdirectory. ${VAR} and ${VAR:-default} are read from the environment. Command-line flags win, and values of the wrong type are reported with the key's name. examples/llmshift.yaml documents every key: default models, judge, repeats, thresholds, severities, scan options, snapshot limits and redaction, fix rounds, pick candidates and prices, watch thresholds and alerts, the digest, and custom providers.
| Code | Meaning |
|---|---|
0 | success |
1 | the check failed: a retired model was found, the pass rate is below the threshold, drift was detected, the new model is riskier, no candidate passes, or doctor found a problem; for update, snapshot and import: nothing could be downloaded, read or imported |
2 | usage or configuration problem, or the provider refused every request the same way (wrong key, model name or URL; an unreachable host). Timeouts and server errors count as failed cases instead (exit 1). Also when nothing failed but some cases could not be checked: a check could not run (an invalid regex, a judge that cannot be reached) |
130 | interrupted |
init adds them to .gitignore. llmshift cache clear deletes the cached responses (they hold your prompts and the answers).snapshot, record and shadow mode redact secrets by default. PII redaction is available with --redact-pii.safety uses only synthetic personal data in its probes.--allow-python, in a separate process. command providers never run through a shell.--judge for semantic judgement.pick are per request on your test set. Cached input, 1-hour cache writes and long prompts cost what the price list gives (the input price, 2 times it for 1-hour writes, and no long-context premium for a model the list gives none). Batch and priority pricing aren't modelled, and token counts estimated from text (about 4 characters per token) are rough.safety compares two models; it is not an absolute safety score. Injection and PII detection are deterministic (a canary or a planted value in the answer).python: provider. Their native APIs are not built in.llmshift update to get the newest data.python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest
The test suite (380 tests) runs offline on Python 3.9 to 3.15. Provider, GitHub and Evals-API tests use a local HTTP server that emulates the real APIs, and tests/test_torture.py feeds every command hostile input.
llmshift watch | Runs a scheduled check against the live model and alerts you (Slack, Teams, GitHub issue, e-mail, ...) when its behaviour drifts. |
max_completion_tokensdeveloper.llmshift/cache, so re-runs are free. Turn this off with --no-cache; llmshift cache clear deletes every cached response.