diff --git a/.github/workflows/canary-staging.yml b/.github/workflows/canary-staging.yml index 37037156..5f1384dc 100644 --- a/.github/workflows/canary-staging.yml +++ b/.github/workflows/canary-staging.yml @@ -63,6 +63,11 @@ jobs: # full_saas.sh branches SECRETS_JSON on which key is present — # MiniMax wins when set. E2E_MINIMAX_API_KEY: ${{ secrets.MOLECULE_STAGING_MINIMAX_API_KEY }} + # Direct-Anthropic alternative for operators who don't want to + # set up a MiniMax account (priority below MiniMax — first + # non-empty wins in test_staging_full_saas.sh's secrets-injection + # block). See #2578 PR comment for the rationale. + E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }} # OpenAI fallback — kept wired so an operator-dispatched run with # E2E_RUNTIME=hermes overridden via workflow_dispatch can still # exercise the OpenAI path without re-editing the workflow. @@ -97,8 +102,20 @@ jobs: # missing" message at the top. case "${E2E_RUNTIME}" in claude-code) - required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" - required_secret_value="${E2E_MINIMAX_API_KEY:-}" + # Either MiniMax OR direct-Anthropic works — first + # non-empty wins in the test script's secrets-injection + # priority chain. Operators only need to set ONE of these + # secrets; we don't force a choice between them. + if [ -n "${E2E_MINIMAX_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" + required_secret_value="${E2E_MINIMAX_API_KEY}" + elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="${E2E_ANTHROPIC_API_KEY}" + else + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY or MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="" + fi ;; langgraph|hermes) required_secret_name="MOLECULE_STAGING_OPENAI_KEY" diff --git a/.github/workflows/continuous-synth-e2e.yml b/.github/workflows/continuous-synth-e2e.yml index 5964693f..b9759c59 100644 --- a/.github/workflows/continuous-synth-e2e.yml +++ b/.github/workflows/continuous-synth-e2e.yml @@ -119,6 +119,11 @@ jobs: # tests/e2e/test_staging_full_saas.sh branches SECRETS_JSON on # which key is present — MiniMax wins when set. E2E_MINIMAX_API_KEY: ${{ secrets.MOLECULE_STAGING_MINIMAX_API_KEY }} + # Direct-Anthropic alternative for operators who don't want to + # set up a MiniMax account (priority below MiniMax — first + # non-empty wins in test_staging_full_saas.sh's secrets-injection + # block). See #2578 PR comment for the rationale. + E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }} # OpenAI fallback — kept wired so operators can dispatch with # E2E_RUNTIME=langgraph or =hermes and still have a working # canary path. The script picks the right blob shape based on @@ -149,13 +154,21 @@ jobs: exit 1 fi - # LLM-key requirement is per-runtime: claude-code uses MiniMax - # (MOLECULE_STAGING_MINIMAX_API_KEY), langgraph + hermes use - # OpenAI (MOLECULE_STAGING_OPENAI_KEY). + # LLM-key requirement is per-runtime: claude-code accepts + # EITHER MiniMax OR direct-Anthropic (whichever is set first), + # langgraph + hermes use OpenAI (MOLECULE_STAGING_OPENAI_KEY). case "${E2E_RUNTIME}" in claude-code) - required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" - required_secret_value="${E2E_MINIMAX_API_KEY:-}" + if [ -n "${E2E_MINIMAX_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" + required_secret_value="${E2E_MINIMAX_API_KEY}" + elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="${E2E_ANTHROPIC_API_KEY}" + else + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY or MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="" + fi ;; langgraph|hermes) required_secret_name="MOLECULE_STAGING_OPENAI_KEY" diff --git a/.github/workflows/e2e-staging-saas.yml b/.github/workflows/e2e-staging-saas.yml index 2c252d10..8cbe468b 100644 --- a/.github/workflows/e2e-staging-saas.yml +++ b/.github/workflows/e2e-staging-saas.yml @@ -93,6 +93,11 @@ jobs: # OpenAI quota collapse no longer wedges the gate. Mirrors the # canary-staging.yml + continuous-synth-e2e.yml migrations. E2E_MINIMAX_API_KEY: ${{ secrets.MOLECULE_STAGING_MINIMAX_API_KEY }} + # Direct-Anthropic alternative for operators who don't want to + # set up a MiniMax account (priority below MiniMax — first + # non-empty wins in test_staging_full_saas.sh's secrets-injection + # block). See #2578 PR comment for the rationale. + E2E_ANTHROPIC_API_KEY: ${{ secrets.MOLECULE_STAGING_ANTHROPIC_API_KEY }} # OpenAI fallback — kept wired so an operator-dispatched run with # E2E_RUNTIME=hermes or =langgraph via workflow_dispatch can still # exercise the OpenAI path. @@ -128,8 +133,19 @@ jobs: # clean "secret missing" message at the top. case "${E2E_RUNTIME}" in claude-code) - required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" - required_secret_value="${E2E_MINIMAX_API_KEY:-}" + # Either MiniMax OR direct-Anthropic works — first + # non-empty wins in the test script's secrets-injection + # priority chain. + if [ -n "${E2E_MINIMAX_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY" + required_secret_value="${E2E_MINIMAX_API_KEY}" + elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then + required_secret_name="MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="${E2E_ANTHROPIC_API_KEY}" + else + required_secret_name="MOLECULE_STAGING_MINIMAX_API_KEY or MOLECULE_STAGING_ANTHROPIC_API_KEY" + required_secret_value="" + fi ;; langgraph|hermes) required_secret_name="MOLECULE_STAGING_OPENAI_KEY" diff --git a/tests/e2e/test_staging_full_saas.sh b/tests/e2e/test_staging_full_saas.sh index 5754b04d..ec6bbf5e 100755 --- a/tests/e2e/test_staging_full_saas.sh +++ b/tests/e2e/test_staging_full_saas.sh @@ -321,8 +321,9 @@ tenant_call() { # ─── 5. Provision parent workspace ───────────────────────────────────── # Inject the LLM provider key so the runtime can authenticate at boot. -# Branch by which secret is set so the script supports both paths -# without forcing every dispatch to ship both keys: +# Branch by which secret is set so the script supports multiple paths +# without forcing every dispatch to ship them all. Priority order +# matters — first non-empty wins: # # E2E_MINIMAX_API_KEY → claude-code MiniMax path. Cheapest, default # for the cron canary post-2026-05-03. Routes via the claude-code @@ -334,6 +335,15 @@ tenant_call() { # collisions when a user runs MiniMax + Z.ai workspaces side-by- # side). # +# E2E_ANTHROPIC_API_KEY → claude-code direct-Anthropic path (added +# 2026-05-04 after #2578 left the operator with an awkward choice +# between paying OpenAI's billing top-up and registering a new +# MiniMax account). Lower friction than MiniMax for operators +# who already have an Anthropic API key for their own Claude +# Code session. Pricier per-token than MiniMax but billing is +# still independent of MOLECULE_STAGING_OPENAI_KEY. Pinned to the +# claude-code runtime — hermes/langgraph use OpenAI-shaped envs. +# # E2E_OPENAI_API_KEY → langgraph + hermes paths. Kept as fallback # for operator dispatches that explicitly want to exercise the # OpenAI path. The HERMES_* fields pin hermes-agent's bridge to @@ -341,7 +351,7 @@ tenant_call() { # resolves openai/* → openrouter.ai and 401s). MODEL_PROVIDER # follows workspace/config.py:258's 'provider:model' format. # -# Both empty → '{}' (workspace will fail at first turn with an +# All empty → '{}' (workspace will fail at first turn with an # expected, actionable auth error rather than masking the test). SECRETS_JSON='{}' if [ -n "${E2E_MINIMAX_API_KEY:-}" ]; then @@ -352,6 +362,25 @@ print(json.dumps({ 'MINIMAX_API_KEY': k, })) ") +elif [ -n "${E2E_ANTHROPIC_API_KEY:-}" ]; then + # Direct Anthropic path — claude-code adapter reads ANTHROPIC_API_KEY + # natively when ANTHROPIC_BASE_URL is unset. Useful for operators + # who already have an Anthropic API key (e.g. for their own Claude + # Code session) and want to avoid setting up a separate MiniMax + # account just for E2E. Pricier per-token than MiniMax but billing + # is still independent of MOLECULE_STAGING_OPENAI_KEY, so an OpenAI + # quota collapse doesn't wedge this path. Pinned to the claude-code + # runtime: hermes/langgraph use OpenAI-shaped envs and won't honour + # ANTHROPIC_API_KEY without further wiring (out of scope for this + # branch; if you need a hermes/Anthropic path, dispatch with + # E2E_RUNTIME=hermes + E2E_OPENAI_API_KEY pointing at a working key). + SECRETS_JSON=$(python3 -c " +import json, os +k = os.environ['E2E_ANTHROPIC_API_KEY'] +print(json.dumps({ + 'ANTHROPIC_API_KEY': k, +})) +") elif [ -n "${E2E_OPENAI_API_KEY:-}" ]; then SECRETS_JSON=$(python3 -c " import json, os