feat: route provider prefixes to the platform they name - #971
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Oddish previewCommit:
Vercel deployment URL: https://oddish-d6zh1z935.oddish.app Plan:
This comment is updated by the PR Preview workflow. |
|
Live preview verification (one task, both new routes), run against this PR's preview deployment:
Same minimal file-write task for both; verifier asserts exact file content. Submitted with the branch CLI against 🤖 Generated with Claude Code |
…y attribution for anthropic/ trials Review follow-ups: the analyzer client now warns about the public OpenAI route only when the global default drives it (an explicit openai/ id is an intentional per-model choice), matching the Harbor agent path. And trial_llm_key_hash stamps the platform ANTHROPIC_API_KEY for anthropic/-prefixed trials even when a resolved BYOK overlay carries the user's key -- the runner pins the platform credential for the prefix (exactly like anthropic-hdo/), so the overlay never funds those runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
…e prefix in the analyzer Review follow-ups: bare OpenAI-family ids now take their concurrency bucket from the transport the global default resolves to (azure/<slug> under the Azure default, sharing the deployment quota with explicit azure/ ids) instead of always landing in the public-platform bucket. The analyzer strips the openai/ transport prefix from the wire model on the public route (the platform API only knows bare slugs), and both governance-warning sites stay quiet for explicitly prefixed ids even when the global default is public. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
…se azure_openai/ queue spelling Review follow-ups: the override-Harbor child builds a plain AgentConfig and never runs _build_agent_config's injectors, so anthropic/ and anthropic-hdo/ trials now get their direct-API routing (key, Bedrock toggles blanked, claude-code model pins) as ambient child env from _runtime_env_overrides — previously such trials could execute on Bedrock while attribution said direct API (the hdo gap predates this change). And azure_openai/<slug> queue keys collapse to azure/<slug>: both spellings name one transport, so identical Azure workloads share one concurrency bucket. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
Review follow-up: the API analyzer client sent its model id to the Anthropic SDK unchanged, so an anthropic/-prefixed id (a canonical stored id after the prefix-routing change) would reach the API as an invalid wire model. The client now normalizes at construction with to_anthropic_api_model_id -- the prefix is stripped and Bedrock-shaped ids map to their dateless API id, the same normalization the CLI analyzer path applies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
73a8f5d to
9d4ed40
Compare
Review follow-up: a trial and an analyzer block normalize the same `anthropic/<model>` id through different helpers. The trial path uses anthropic_platform_bare_model_id, which maps the dotted marketing spelling to the dashed API id; to_anthropic_api_model_id stripped the prefix and kept the dots, so `anthropic/claude-opus-4.8` reached the Anthropic SDK as `claude-opus-4.8` and 404s -- from the API analyzer client, the CLI analyzer path, and every other direct-API caller. The dotted-alias rule now lives in one helper both call, so a prefixed id resolves identically on either path. It fires only on a Claude id behind an anthropic/ or claude/ prefix: OpenAI slugs (gpt-5.3-codex), Bedrock ids (anthropic.claude-...-v1:0), ARNs and openrouter/ pricing candidates are legitimately dotted and pass through untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz
* fix(ci): coordinate staging deploy with the database bootstrap * fix(ci): sync guard runs on main pushes and gives safe recovery advice * fix(ci): deploy staging after the bootstrap mirror The shared staging-environment concurrency group serializes staging-deploy with staging-db-bootstrap but does not order them. When the deploy wins the lock on a bootstrap-triggering push, it migrates the pre-mirror branch and nothing deploys after the mirror. The bootstrap now dispatches its own Staging Deploy at the end, so the post-mirror deploy no longer depends on which run started first. It is skipped after a password reset, where the stale STAGING_DATABASE_URL secret would fail the deploy at migrations. Pass --ref staging in the printed deploy recipe so the runbook does not depend on the repository default branch staying staging. State the supersede rule where it matters: only the staging tip is guaranteed a deploy run, so the promotion preflight now explains that a commit staging moved past may never have deployed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz * fix(ci): publish the staging DB secret before the mirror The password-reset recovery recipe reads the new URL from the oddish-staging-db Modal secret, but that secret was published after the multi-hour mirror. An operator who acted on the printed warning got the pre-reset URL, and a failure between the reset and the publish left the new password with no durable copy at all, because the Supabase API redacts it. Publish the secret immediately after the branch step resolves the URL. Every later step reads steps.branch.outputs.db_url directly, so none of them depend on the old position. Add a credential check to Staging Deploy. A deploy queued in the shared group still runs after a password reset and still fails, which is correct, since staging is not deployed at that commit until a human refreshes the secret. The check names that cause instead of leaving a bare Alembic error, and it fails rather than skips so the promotion gate stays honest. Record that GitHub holds one pending run per concurrency group across both workflows, so a queued bootstrap can be cancelled by a newer queued deploy and needs dispatching again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz * fix(ci): stop the recovery recipe from blanking the staging secret The printed recipe piped modal run through grep straight into gh secret set. gh reads stdin verbatim and never checks it for emptiness, and the value is sealed client-side so the API cannot check it either. A modal run that failed or emitted no URL therefore replaced STAGING_DATABASE_URL with an empty value, and the pipeline still exited clean because the status came from gh. The recipe now captures the URL, checks it is non-empty, and only then writes the secret and dispatches the deploy. It uses a guard rather than pipefail, because the recipe is pasted into an operator's own shell and must not change that shell's options. Correct the emit script docstring to match: modal run writes its own progress output around the URL, so the line has to be selected and checked before any secret is written. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz * fix(ci): stop restarting staging compute that is already correct The compute-return step applied the ci_large addon on every bootstrap run. It never read the current tier, so its claim of being a no-op described the intended end state and not the code. Applying an addon restarts the branch, which put a restart of unknown duration in front of the deploy this run dispatches, on every run rather than only after manual tier drift. Read the current tier and send nothing when it already matches. Only a tier that positively reads as ci_large skips the request. A failed read falls through and applies, which is the behaviour this step has always had. Retry the staging deploy credential check instead of probing once. A real tier change still restarts the branch, and this project has seen connections refused for seconds after health reports green, so a single attempt blamed a stale credential for a database that was merely coming back. The check now retries for 300s and its failure message names both causes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz * fix(ci): keep the live staging secret off a mid-teardown database Publishing oddish-staging-db right after the password reset gave the secret the live app mounts a working credential while the schema was being replaced and the tables reloaded. A cold start could then authenticate against a half-built database, where before the reset left the app failing closed. Split the two jobs that publish was doing. A recovery copy of the URL goes to oddish-staging-db-pending immediately, which nothing mounts, so the new password stays recoverable if a later step dies. The live secret is published only after the mirror completes. The emit script now takes a secret name from STAGING_DB_SECRET, and the printed recipe reads the pending copy. Run the branch sync guard on staging pushes too. Rewriting staging can drop main's ancestry without any push to main, and that path waited for the daily cron. Fail the staging deploy credential check immediately on a rejected password. Retrying a deterministic rejection held the shared concurrency lock for five minutes and delayed the recovery deploy. Refusals, timeouts and a starting database still retry to the deadline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): create the preview deployment record as the workflow * fix(ci): record the preview deployment against the head commit * feat(ci): skip preview provisioning for promotion pull requests * fix(ci): promotion short-circuit requires the repository's own staging branch
* Enable opencode on closed-internet trials via egress allowlist
Stock opencode self-installs (nvm/Node/opencode-ai) at trial start and
declares no Oddish egress, so on allow_internet=false trials Harbor's
Modal firewall blackholes both its install and its model calls: the trial
dies at DNS during agent setup ("curl: (6) Could not resolve host:
raw.githubusercontent.com") before the model is ever reached.
Add an OddishOpenCode wrapper implementing required_outbound_domains
(install-bootstrap hosts + model transport host via outbound_hosts_for_model,
which already resolves OpenRouter -> openrouter.ai), and route `-a opencode`
through it in _build_agent_config. Mirrors the AzureCompatibleCodex fix (#416).
Adds routing + egress unit tests and a CHANGELOG entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4FaAPiaXnXCZzDJ8F1bvw
* opencode egress: forward per-trial kwargs to host resolution
Address Cursor Bugbot (medium): required_outbound_domains accepted kwargs but
never forwarded them to outbound_hosts_for_model, so a transport host pinned via
kwargs["extra_env"] (e.g. a custom OPENROUTER_BASE_URL) was not allowlisted and a
closed-internet trial overriding its base URL would still be firewalled after
install. Pass agent_kwargs=kwargs, mirroring AzureCompatibleCodex. Adds a
regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4FaAPiaXnXCZzDJ8F1bvw
* Register opencode install hosts in _AGENT_RUNTIME_HOSTS — the enforced egress path
required_outbound_domains has no consumer in oddish or harbor (verified at
harbor 504c2518 and end-to-end on the PR preview backend: a closed-internet
opencode trial still died at nvm DNS with only the hook declared). The
allowlist actually enforced on closed-internet Modal trials is built by
_inject_restricted_agent_model_hosts as outbound_hosts_for_model (which
already resolves openrouter/<model> -> openrouter.ai) ∪ agent_runtime_hosts,
which reads the static _AGENT_RUNTIME_HOSTS registry. Register
OPENCODE_INSTALL_HOSTS there, keyed by both the stock agent name and the
OddishOpenCode wrapper class name (wrapper routing nulls the name). The hook
stays for interface parity, documented as declarative-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQug9XcmqY2aL5pmjJdRUN
* Route opencode install+model hosts through the environment baseline, mirroring claude-code
The agent-runtime-hosts registration could never work: opencode installs
during agent SETUP, which runs under the environment baseline — the
agent-phase allowlist only applies around agent.run() (verified e2e on the
preview: trials -1060/-1061 still died at nvm DNS with the registry entry
deployed). Mirror the claude-code installer arm instead: merge
OPENCODE_INSTALL_HOSTS + outbound_hosts_for_model into
env_config.extra_allowed_hosts, which harbor folds into the environment
baseline spanning install and run. This also covers legacy closed tasks
([environment] allow_internet=false -> no-network baseline, no dynamic
restricted agent phase — the GDM SWE-Marathon sample shape), where no other
channel grants even the model host. On modern swe-marathon-shaped tasks the
public baseline ignores the extras, so no install hosts leak into agent run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RQug9XcmqY2aL5pmjJdRUN
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
* feat(ci): promote staging to main on a pull request comment * fix(ci): gate the promote push on a job env value * fix(ci): promote push works and reports its real outcome * docs: hotfix procedure, code owners, promote command * fix(ci): tighten the promote trigger, gate the reply, scope concurrency Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz * docs(ci): header states the auth gate on replies * fix(ci): accept a promote command followed by an explanation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PtexagKz2Si6SVLZoFKrz --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Review follow-up: llm_key_fingerprint only treated "azure" as Oddish-wired, so a trial whose provider surfaces as "azure_openai" (the normalizer passes that spelling through verbatim) fell through to Harbor's PROVIDER_KEYS and lost its funding stamp. Both spellings now map to AZURE_OPENAI_API_KEY -- settings first, worker env fallback -- the same dual-row pattern the runner's redaction map uses so the two surfaces cannot drift apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d13f269. Configure here.
| try: | ||
| if provider == "openai": | ||
| raw = _openai_platform_key() | ||
| raw = _openai_platform_key(model) |
There was a problem hiding this comment.
Platform Anthropic stamp ignores settings
Medium Severity
anthropic/ trials are funded with _resolve_anthropic_platform_api_key, which prefers settings.anthropic_api_key over ANTHROPIC_API_KEY, but platform_key_hash_for_provider("anthropic") still hashes only os.environ. When the configured settings key is set and the env var is missing or different, llm_key_hash no longer matches the key that actually paid for the run.
Reviewed by Cursor Bugbot for commit d13f269. Configure here.




Description
Each model-id prefix now selects one transport. The prefix always selects the transport that it names.
openai/<slug>OPENAI_API_KEY(already in the secret)azure/<slug>AZURE_OPENAI_*and the deployment mapbedrock/<id>anthropic/<id>ANTHROPIC_API_KEYanthropic-hdo/<id>ANTHROPIC_HDO_API_KEY(no change)The route for a bare model id does not change:
gptid obeysODDISH_OPENAI_PROVIDER. The default is Azure.claudeid goes to AWS Bedrock.claude/stays an alias for Bedrock.Operation
get_openai_route_for_model()finds the transport for an OpenAI-family model id. The environment mint, the Azure deployment rewrite, the codex wrapper selection, the egress hosts, and the analyzer client use this function.anthropic/id does not use the Bedrock table. It has its own queue. The worker sets the direct-API key and removes the Bedrock settings. Old model ids that have a date are again possible on this route.azure/ids. The deployment map keeps itsopenai/keys.claude-sonnet-5.Effects on operation
openai/id went to Azure before this change. It now goes to the public OpenAI platform.anthropic/id went to Bedrock before this change. It now goes to the direct Anthropic API.openai/gpt-5.6-*and Codex CLI 0.145.0 or a later version.Tests
main.openai/gpt-5.3-codexand claude-code withanthropic/claude-haiku-4-5.ODDISH_MODEL_CONCURRENCY_OVERRIDES. Bare-id queue keys now follow their transport.🤖 Generated with Claude Code
https://claude.ai/code/session_012KtVx8UnLzag4idP3GDqa3
Note
High Risk
This changes which external APIs receive trial data, breaks prior
openai/andanthropic/semantics, and touches auth env injection, egress, concurrency keys, and cost attribution—operators must revisit deployment maps andODDISH_MODEL_CONCURRENCY_OVERRIDES.Overview
Prefix-driven routing replaces a mostly global OpenAI default:
get_openai_route_for_model()sendsopenai/<slug>to the public API,azure//azure_openai/to Azure, and bare GPT ids toODDISH_OPENAI_PROVIDER. Harbor agents, analyzer OpenAI clients, runtime env minting, Codex Azure wrapping, and egress allowlists all follow that per-model route.anthropic/<id>is a new opt-in path (likeanthropic-hdo/) to the direct Anthropic API with the platformANTHROPIC_API_KEY, bypassing Bedrock normalization, using its own queue/concurrency bucket, pinning runner/child env (Bedrock toggles cleared), and stamping cost to the platform key even when BYOK is present. Bareclaude-*ids still canonicalize to Bedrock;claude/andbedrock/remain Bedrock aliases.Breaking semantics:
openai/…no longer implies Azure;anthropic/…no longer collapses to Bedrock. Queue keys for bare OpenAI-family ids match the resolved transport (azure/<slug>vsopenai/<slug>),azure_openai/aliases toazure/, and Azure deployment lookup acceptsazure/ids against existingopenai/map keys.Supporting fixes:
claude-sonnet-5Bedrock mapping, dotted Claude marketing ids normalized on direct-API paths, analyzer Anthropic wire ids viato_anthropic_api_model_id, and ephemeral Harbor children get direct-Anthropic ambient env when prefixes require it.Reviewed by Cursor Bugbot for commit d13f269. Bugbot is set up for automated code reviews on this repo. Configure here.