Skip to content

feat: route provider prefixes to the platform they name - #971

Open
dot-agi wants to merge 15 commits into
stagingfrom
feat/provider-prefix-routing
Open

feat: route provider prefixes to the platform they name#971
dot-agi wants to merge 15 commits into
stagingfrom
feat/provider-prefix-routing

Conversation

@dot-agi

@dot-agi dot-agi commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Description

Each model-id prefix now selects one transport. The prefix always selects the transport that it names.

Prefix Transport Credential
openai/<slug> The public OpenAI platform OPENAI_API_KEY (already in the secret)
azure/<slug> Azure OpenAI AZURE_OPENAI_* and the deployment map
bedrock/<id> AWS Bedrock The Bedrock credentials (no change)
anthropic/<id> The direct Anthropic API The platform ANTHROPIC_API_KEY
anthropic-hdo/<id> The direct Anthropic API, HDO org ANTHROPIC_HDO_API_KEY (no change)

The route for a bare model id does not change:

  • A bare gpt id obeys ODDISH_OPENAI_PROVIDER. The default is Azure.
  • A bare claude id goes to AWS Bedrock.
  • The prefix claude/ stays an alias for Bedrock.

Operation

  • The function 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.
  • An 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.
  • The system writes the hash of the key that paid for a trial. The model id shows the system which key that is.
  • The Azure resolver accepts azure/ ids. The deployment map keeps its openai/ keys.
  • The Bedrock table has the new model claude-sonnet-5.
  • The ephemeral Harbor child gets the direct-API settings in its environment.
  • The queue key for a model agrees with its transport. The two Azure spellings share one queue.

Effects on operation

  • An openai/ id went to Azure before this change. It now goes to the public OpenAI platform.
  • An anthropic/ id went to Bedrock before this change. It now goes to the direct Anthropic API.
  • These changes have effects on cost reports, egress rules, and data governance.
  • Codex CLI 0.144 and later versions use the "Responses Lite" format for the gpt-5.6 models. Azure does not accept this format (openai/codex #31870, #31875, #31882). For these models, use openai/gpt-5.6-* and Codex CLI 0.145.0 or a later version.

Tests

  • The full test suite gave 2935 passed tests. There were zero new failures against main.
  • One live task on the PR preview gave a reward of 1.00 on each new route. The trials were codex with openai/gpt-5.3-codex and claude-code with anthropic/claude-haiku-4-5.
  • Cursor Bugbot examined this change three times. We corrected seven findings and refuted one. The last two examinations found zero new problems.
  • At deployment, examine the keys in 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/ and anthropic/ semantics, and touches auth env injection, egress, concurrency keys, and cost attribution—operators must revisit deployment maps and ODDISH_MODEL_CONCURRENCY_OVERRIDES.

Overview
Prefix-driven routing replaces a mostly global OpenAI default: get_openai_route_for_model() sends openai/<slug> to the public API, azure/ / azure_openai/ to Azure, and bare GPT ids to ODDISH_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 (like anthropic-hdo/) to the direct Anthropic API with the platform ANTHROPIC_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. Bare claude-* ids still canonicalize to Bedrock; claude/ and bedrock/ 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> vs openai/<slug>), azure_openai/ aliases to azure/, and Azure deployment lookup accepts azure/ ids against existing openai/ map keys.

Supporting fixes: claude-sonnet-5 Bedrock mapping, dotted Claude marketing ids normalized on direct-API paths, analyzer Anthropic wire ids via to_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.

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
oddish-app Ready Ready Preview Aug 18, 2026 12:19am

Comment thread oddish/src/oddish/workers/harbor/model_hosts.py
Comment thread oddish/src/oddish/blocks/analyzer/analyzer_llm_client.py
Comment thread oddish/src/oddish/core/llm_key_fingerprint.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Left a non-blocking comment — Cursor Bugbot finished as skipped and reported 3 unresolved findings that need human review; Cursor Security Agent was not present. Assigned reviewers for the provider-routing changes.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Oddish preview

Commit: d3615ba662424ec076607dee63d06c4036d3e5f0

Surface Link Target
Frontend https://pr-971.oddish.app Vercel preview for d3615ba
Backend oddish-pr-971 oddish-pr-971
Database project knyefiwvpwkztoxcwjwk project knyefiwvpwkztoxcwjwk

Vercel deployment URL: https://oddish-d6zh1z935.oddish.app

Plan:

  • Frontend deploy: true
  • Backend deploy: true
  • Migrations: true

This comment is updated by the PR Preview workflow.

@dot-agi

dot-agi commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Live preview verification (one task, both new routes), run against this PR's preview deployment:

Trial Agent Model Transport exercised Status Reward
0 codex openai/gpt-5.3-codex Public OpenAI platform (api.openai.com) success 1.00
1 claude-code anthropic/claude-haiku-4-5 Direct Anthropic API (Bedrock bypassed) success 1.00

Same minimal file-write task for both; verifier asserts exact file content. Submitted with the branch CLI against oddish-pr-971-api (preview DB + preview workers), using a temporary preview-only API key that expires within 6h.

🤖 Generated with Claude Code

Comment thread oddish/src/oddish/config.py
Comment thread oddish/src/oddish/workers/harbor/agent_config.py
Comment thread oddish/src/oddish/blocks/analyzer/analyzer_llm_client.py
Comment thread oddish/src/oddish/workers/harbor/runner.py
Comment thread oddish/src/oddish/config.py
@dot-agi
dot-agi requested review from pfbyjy and removed request for charlesyhuang July 30, 2026 23:18
@dot-agi
dot-agi changed the base branch from main to staging July 31, 2026 18:24
@dot-agi
dot-agi changed the base branch from staging to main July 31, 2026 19:00
Comment thread oddish/src/oddish/blocks/analyzer/analyzer_llm_client.py
dot-agi and others added 4 commits August 4, 2026 23:52
…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
@dot-agi
dot-agi force-pushed the feat/provider-prefix-routing branch from 73a8f5d to 9d4ed40 Compare August 4, 2026 18:23
Comment thread oddish/src/oddish/blocks/analyzer/analyzer_llm_client.py
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
dot-agi and others added 5 commits August 5, 2026 03:33
* 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>
Comment thread oddish/src/oddish/core/llm_key_fingerprint.py
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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d13f269. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants