From 904d93662653857bfd2fda18f5d6b9ee0f3ed852 Mon Sep 17 00:00:00 2001 From: djl11 Date: Wed, 29 Jul 2026 20:27:27 +0100 Subject: [PATCH 1/2] Adopt the OpenRouter-only LLM access rule Shared rule from global-agent-rules: the direct OpenAI account is inactive, so 'openai/@openrouter' is the only supported route. Co-Authored-By: Claude Opus 5 --- .agents/global-rules | 2 +- .agents/shared.txt | 2 ++ AGENTS.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/.agents/global-rules b/.agents/global-rules index 69f492154..068750ae6 160000 --- a/.agents/global-rules +++ b/.agents/global-rules @@ -1 +1 @@ -Subproject commit 69f492154c5335d618a3659c13d0d1da82bb4409 +Subproject commit 068750ae687660ff6a641897f9b1df7aea6796a3 diff --git a/.agents/shared.txt b/.agents/shared.txt index 5fb0ce897..be133e29e 100644 --- a/.agents/shared.txt +++ b/.agents/shared.txt @@ -13,5 +13,7 @@ worktree-direct-commits.md git-history-for-context.md shared-agent-context.md +openrouter-only-llm-access.md + # --- specific to this repo --- python-formatting-and-pre-commit.md diff --git a/AGENTS.md b/AGENTS.md index 6e531acc7..691f71c63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -573,3 +573,58 @@ Cite **user**, **tool**, **date**, and **path** so a human can open the same ses - Do not scrub or rewrite historical transcripts. - Do not push/sync unless the user asked you to. - Do not grep `yours/` unless the user asked for local-only context. + +# OpenAI is reached only through OpenRouter + +Every LLM call in every repo routes through **OpenRouter**, using +`OPENROUTER_API_KEY`. The company's direct OpenAI account is not active — it +answers `429 billing_not_active` — so any code path that talks to OpenAI +natively is dead code that fails slowly. + +## Canonical endpoint form + +``` +openai/@openrouter # openai/gpt-5.6-terra@openrouter +``` + +Never `@openai`. In UniLLM, `@openai` and `@openrouter` are distinct +providers (`unillm/endpoints/utils.py`): `@openrouter` resolves through the +OpenRouter catalog, `@openai` resolves to a native OpenAI endpoint and litellm +sends it straight to OpenAI with `OPENAI_API_KEY`. + +**The alias changed meaning.** `gpt-*@openai` used to be *transported* via +OpenRouter inside UniLLM. It now means native OpenAI. Model strings written +before that change did not move — they silently re-pointed at a dead account. +The Orchestra migration `2026-08-13-00-00_openrouter_model_endpoints.py` +rewrote stored assistant endpoints for exactly this reason; source code was not +covered by it. + +## Why this fails slowly rather than loudly + +OpenAI reports the billing fault as **HTTP 429**, the same status as +rate-limiting. UniLLM's `_is_retryable` classifies 429 as transient and retries +`UNILLM_TRANSIENT_RETRY_COUNT` (default 6) times with 1/2/4/8/16/32s backoff — +63s of sleeping per call, multiplied by litellm's own internal retries, before +it finally raises. Under any concurrency this is indistinguishable from a hang, +and scheduled jobs look stuck rather than broken. Do not "fix" such a stall by +raising a timeout; check the endpoint's provider suffix first. + +## Hard rules + +- New LLM call sites use `openai/@openrouter`. Non-OpenAI providers + (Anthropic, Google, …) are unaffected by this rule and keep their own routing. +- Never read `OPENAI_API_KEY` directly, and never construct `openai.OpenAI()` + against it, in application code. +- Env defaults and `.env.example` entries carry the `@openrouter` form, so a + fresh checkout cannot inherit a dead route. +- When a provider call stalls for ~a minute and then fails, suspect a native + provider suffix before suspecting the network. + +## The one legitimate direct-OpenAI path + +Masked image edits (`images.edit` with `gpt-image-2`) have no OpenRouter +equivalent — OpenRouter's unified Image API does not expose the mask parameter. +That path may use a separately-named credential (`OPENAI_DIRECT_API_KEY`), must +never fall back to reading `OPENAI_API_KEY`, and must degrade loudly when the +credential is absent. It is the only exception; adding another needs an +explicit reason, not convenience. From aa194c10c52855aa61dcad188431a200ca346272 Mon Sep 17 00:00:00 2001 From: djl11 Date: Wed, 29 Jul 2026 23:06:24 +0100 Subject: [PATCH 2/2] Ignore the uv.lock stub uv writes into this Poetry project Poetry-managed, and nothing here consumes uv or uv.lock. Any uv command that resolves the workspace still writes a stub, because uv reads PEP 621 [project].requires-python and the constraint lives under [tool.poetry.dependencies]. orchestra had the same exposure and the stub was committed by accident twice before 47dd1d3f; ignore it here pre-emptively. Co-Authored-By: Claude Opus 5 --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 4b97db553..006367dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,10 @@ __pycache__/ *.tmp .vscode/ test.py + +# This project is Poetry-managed. Any uv invocation that resolves the workspace +# (uv run / uv sync / uv lock) writes a stub uv.lock here, because uv reads PEP +# 621 [project].requires-python and this pyproject.toml declares the constraint +# under [tool.poetry.dependencies]. The stub carries no package data; ignore it +# so it cannot be committed by accident (see orchestra 47dd1d3f). +/uv.lock