From 98f883cb999a5f955b76bc0ac4ec12c4ed2887f0 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Mon, 4 May 2026 00:51:14 -0700 Subject: [PATCH] e2e: add direct-Anthropic LLM-key path alongside MiniMax + OpenAI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third secrets-injection branch in test_staging_full_saas.sh behind a new E2E_ANTHROPIC_API_KEY env var, wired into all three auto-running E2E workflows (canary-staging, e2e-staging-saas, continuous-synth-e2e) via a new MOLECULE_STAGING_ANTHROPIC_API_KEY repo secret slot. Operator motivation: after #2578 (the staging OpenAI key went over quota and stayed dead 36+ hours) we shipped #2710 to migrate the canary + full-lifecycle E2E to claude-code+MiniMax. Discovered post- merge that MOLECULE_STAGING_MINIMAX_API_KEY had never been set after the synth-E2E migration on 2026-05-03 either — synth has been red the whole time, not just OpenAI quota. Setting up a MiniMax billing account from scratch is non-trivial (needs platform-specific signup, KYC, top-up). Operators who already have an Anthropic API key for their own Claude Code session can now just set MOLECULE_STAGING_ANTHROPIC_API_KEY and have all three auto-running E2E gates green within one cron firing. Priority chain in test_staging_full_saas.sh (first non-empty wins): 1. E2E_MINIMAX_API_KEY → MiniMax (cheapest) 2. E2E_ANTHROPIC_API_KEY → direct Anthropic (cheaper than gpt-4o, lower setup friction than MiniMax) 3. E2E_OPENAI_API_KEY → langgraph/hermes paths Verify-key case-statement in all three workflows accepts EITHER MiniMax OR Anthropic for runtime=claude-code; error message names both options so operators know they don't have to register a MiniMax account if they already have an Anthropic key. Pinned to runtime=claude-code — hermes/langgraph use OpenAI-shaped envs and won't honour ANTHROPIC_API_KEY without further wiring. After this lands + secret is set, the dispatched canary verifies the new path: gh workflow run canary-staging.yml --repo Molecule-AI/molecule-core --ref staging --- .github/workflows/canary-staging.yml | 21 +++++++++++-- .github/workflows/continuous-synth-e2e.yml | 23 ++++++++++---- .github/workflows/e2e-staging-saas.yml | 20 +++++++++++-- tests/e2e/test_staging_full_saas.sh | 35 ++++++++++++++++++++-- 4 files changed, 87 insertions(+), 12 deletions(-) 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