From bf0d902dfc5369bd025f64808d96fe6cb473ea75 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 2 Sep 2026 15:11:08 -0300 Subject: [PATCH] docs(providers): register providers removed at their operator's request and guard against reintroduction (#12478) Adds docs/reference/REMOVED_PROVIDERS.md (policy + register: puter #10210, the keyless provider removed in #12440), links it from the docs index and AGENTS.md's provider checklist, and adds a regression test that fails if any registered id, alias or domain shows up again in the provider catalogs, the executor map or the registry/executor sources. Co-authored-by: Markus Hartung --- AGENTS.md | 1 + docs/README.md | 1 + docs/reference/REMOVED_PROVIDERS.md | 60 ++++++++++ docs/reference/meta.json | 1 + .../unit/removed-providers-blocklist.test.ts | 107 ++++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 docs/reference/REMOVED_PROVIDERS.md create mode 100644 tests/unit/removed-providers-blocklist.test.ts diff --git a/AGENTS.md b/AGENTS.md index 26b89dee89..d0ac2952b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -343,6 +343,7 @@ Documentation must describe verified behavior, not plausible behavior. ### Adding a New Provider +0. Check `docs/reference/REMOVED_PROVIDERS.md` first — providers removed at their operator's request must never be reintroduced (guarded by `tests/unit/removed-providers-blocklist.test.ts`) 1. Register in `src/shared/constants/providers.ts` (Zod-validated at load) 2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`) 3. Add translator in `open-sse/translator/` if non-OpenAI format diff --git a/docs/README.md b/docs/README.md index 6e531b9828..3c40dfaea9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -79,6 +79,7 @@ Lookup material — API surface, environment variables, CLI flags, provider cata - [API_REFERENCE.md](reference/API_REFERENCE.md) — REST API endpoints and shapes. - [PROVIDER_REFERENCE.md](reference/PROVIDER_REFERENCE.md) — auto-generated provider catalog (do not edit by hand). +- [REMOVED_PROVIDERS.md](reference/REMOVED_PROVIDERS.md) — providers removed at their operator's request; never reintroduce without written permission. - [PROVIDER_PLUGIN_MANIFEST.md](reference/PROVIDER_PLUGIN_MANIFEST.md) — sidecar-safe provider plugin contract for Bifrost and CLIProxyAPI migration. - [openapi.yaml](openapi.yaml) — OpenAPI spec for the public API. - [ENVIRONMENT.md](reference/ENVIRONMENT.md) — environment variables reference. diff --git a/docs/reference/REMOVED_PROVIDERS.md b/docs/reference/REMOVED_PROVIDERS.md new file mode 100644 index 0000000000..e03283bd43 --- /dev/null +++ b/docs/reference/REMOVED_PROVIDERS.md @@ -0,0 +1,60 @@ +# Providers removed at their operator's request + +Some services were integrated into OmniRoute and later removed because the people who run +them asked for it. This page is the durable record of those removals. Its only purpose is to +keep them from coming back by accident: a contributor who finds an old fork, a cached npm +tarball, an archived issue or a "restore provider X" request needs one place that says **do +not reintroduce**. + +This page is **not** a list of dead or discontinued services. Those are tracked in +[`FREE_TIERS.md`](FREE_TIERS.md) ("Removed / no free tier") and can come back if the service +does. The entries below can only come back with written permission from the operator named in +the request, and that permission must be linked from the entry. + +## Policy + +1. **A takedown request from a service operator is honored, not negotiated.** OmniRoute is + not affiliated with any upstream service. When the operator of a service asks for the + integration to go, it goes, whether the integration used an official API or not. +2. **"Removed" means every surface OmniRoute controls.** Executor, registry entry, provider + id and alias, model list, endpoints, environment variables, icon, dashboard cards, the + generated provider reference, `FREE_TIERS.md`, the environment reference, README counts, + `llm.txt` mirrors, dedicated tests and golden snapshots, code comments, CHANGELOG bullets + (with a ledgered reconciliation, see `config/release/changelog-reconciliations.json`), + GitHub Releases notes, the wiki, and the GitHub issues, discussions and pull requests whose + subject was that provider (issues and discussions deleted; pull requests retitled, their + description replaced and the thread locked, because GitHub cannot delete pull requests). +3. **Never reintroduce an entry on this page without written permission.** That includes + adding the id or alias back to any provider catalog, adding the domains to an executor, + accepting a contributor PR that "restores" it, adding it to the free-model catalog, or + documenting a manual way to reach it through OmniRoute. Close such PRs and issues with a + link to this page. +4. **Keep the entry minimal.** Record only what a reviewer needs to recognize a + reintroduction: identifiers, domains, dates and the pull request that did the removal. + Do not describe how the integration worked. +5. **The regression guard is `tests/unit/removed-providers-blocklist.test.ts`.** It fails when + any identifier or domain below shows up again in the provider catalogs, the executor map or + the provider registry sources. Add the new identifiers to that test in the same PR that + adds a row here. + +## Register + +| Removed on | Provider id | Alias | Domains | Requested by | Removal PR | Notes | +| ---------- | ----------- | ------ | --------------------------------------- | ------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| 2026-08-12 | `puter` | `pu` | `puter.com` | Puter's owner (Nariman Jelveh) | [#10210](https://github.com/diegosouzapw/OmniRoute/pull/10210) | API-key provider. Migration `152_remove_puter_provider.sql` cleans stored config. | +| 2026-09-02 | `theoldllm` | `tllm` | `theoldllm.com`, `theoldllm.vercel.app` | The service operator (support email) | [#12440](https://github.com/diegosouzapw/OmniRoute/pull/12440) | Keyless provider. Written request received 2026-08-30. Dedicated issues and discussion deleted, PRs retitled. | + +## Adding an entry + +When a new takedown request arrives: + +1. Confirm the request comes from the operator of the service (their support address or a + domain they control), and keep the message privately. +2. Remove the integration following the checklist in policy item 2. Use + [#12440](https://github.com/diegosouzapw/OmniRoute/pull/12440) as the reference for a + keyless provider and [#10210](https://github.com/diegosouzapw/OmniRoute/pull/10210) for an + API-key provider with stored connections (add a migration). +3. Add one row to the table above and the identifiers to + `tests/unit/removed-providers-blocklist.test.ts`, in the same PR. +4. Reply to the operator once the PR is merged, listing what was removed and what OmniRoute + cannot change (already-published npm and Docker versions, git history, third-party forks). diff --git a/docs/reference/meta.json b/docs/reference/meta.json index cff028c28e..2a96cef15e 100644 --- a/docs/reference/meta.json +++ b/docs/reference/meta.json @@ -8,6 +8,7 @@ "FREE_TIERS", "FREE_PROXIES_API", "PROVIDER_REFERENCE", + "REMOVED_PROVIDERS", "PROVIDER_PLUGIN_MANIFEST", "RELAY_BACKEND_STRATEGY", "RELAY_TROUBLESHOOTING" diff --git a/tests/unit/removed-providers-blocklist.test.ts b/tests/unit/removed-providers-blocklist.test.ts new file mode 100644 index 0000000000..2b7026c101 --- /dev/null +++ b/tests/unit/removed-providers-blocklist.test.ts @@ -0,0 +1,107 @@ +/** + * Regression guard for docs/reference/REMOVED_PROVIDERS.md. + * + * Providers removed at their operator's request must never come back: not in the + * provider catalogs, not in the executor map, not in the registry sources and not + * as an upstream domain in any executor. Keep this list in sync with the table in + * the doc; add the identifiers of a new takedown here in the same PR. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { getProviderById, getProviderByAlias } = + await import("../../src/shared/constants/providers.ts"); +const { hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const { FREE_MODEL_BUDGETS } = await import("../../open-sse/config/freeModelCatalog.data.ts"); + +interface RemovedProvider { + id: string; + alias: string; + domains: string[]; + removalPr: number; +} + +export const REMOVED_PROVIDERS: readonly RemovedProvider[] = [ + { id: "puter", alias: "pu", domains: ["puter.com"], removalPr: 10210 }, + { + id: "theoldllm", + alias: "tllm", + domains: ["theoldllm.com", "theoldllm.vercel.app"], + removalPr: 12440, + }, +]; + +// Source trees where a reintroduction would land. Scanned for ids, aliases and domains. +const SCANNED_DIRS = [ + "open-sse/config/providers", + "open-sse/executors", + "src/shared/constants/providers", +]; + +function walk(dir: string, out: string[] = []): string[] { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) walk(full, out); + else if (/\.(ts|tsx|mts|js|mjs|json)$/.test(entry.name)) out.push(full); + } + return out; +} + +const ROOT = process.cwd(); +const scannedFiles = SCANNED_DIRS.flatMap((dir) => walk(path.join(ROOT, dir))); + +for (const removed of REMOVED_PROVIDERS) { + test(`removed provider "${removed.id}" (PR #${removed.removalPr}) stays out of the chat registry`, () => { + assert.equal(REGISTRY[removed.id], undefined, `${removed.id} must not be in REGISTRY`); + assert.equal(REGISTRY[removed.alias], undefined, `${removed.alias} must not be in REGISTRY`); + }); + + test(`removed provider "${removed.id}" stays out of the provider catalogs`, () => { + assert.equal(getProviderById(removed.id), undefined, `${removed.id} must not be a provider`); + assert.equal( + getProviderByAlias(removed.alias), + null, + `alias ${removed.alias} must not be reused by any provider` + ); + }); + + test(`removed provider "${removed.id}" has no executor (id or alias)`, () => { + assert.equal(hasSpecializedExecutor(removed.id), false); + assert.equal(hasSpecializedExecutor(removed.alias), false); + }); + + test(`removed provider "${removed.id}" has no free-model catalog entries`, () => { + assert.deepEqual( + FREE_MODEL_BUDGETS.filter((b) => b.provider === removed.id), + [], + `${removed.id} must not appear in FREE_MODEL_BUDGETS` + ); + }); + + test(`removed provider "${removed.id}" identifiers and domains are absent from registry/executor sources`, () => { + const needles = [`"${removed.id}"`, `"${removed.alias}"`, ...removed.domains]; + const offenders: string[] = []; + for (const file of scannedFiles) { + const text = fs.readFileSync(file, "utf8"); + for (const needle of needles) { + if (text.includes(needle)) offenders.push(`${path.relative(ROOT, file)} :: ${needle}`); + } + } + assert.deepEqual( + offenders, + [], + `reintroduction of "${removed.id}" detected — see docs/reference/REMOVED_PROVIDERS.md` + ); + }); +} + +test("the REMOVED_PROVIDERS doc lists every guarded id", () => { + const doc = fs.readFileSync(path.join(ROOT, "docs/reference/REMOVED_PROVIDERS.md"), "utf8"); + for (const removed of REMOVED_PROVIDERS) { + assert.ok(doc.includes(`\`${removed.id}\``), `${removed.id} must have a row in the doc`); + assert.ok(doc.includes(`#${removed.removalPr}`), `PR #${removed.removalPr} must be linked`); + } +});