Skip to content

Commit 075a13a

Browse files
authored
Merge pull request #138 from phoenixvc/fix/prod-anthropic-provider-wiring
fix(prod): wire Anthropic provider into Sluice deployment
2 parents 56ece8f + 04445ab commit 075a13a

6 files changed

Lines changed: 179 additions & 34 deletions

File tree

.github/workflows/deploy-environment.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ on:
111111
required: false
112112
MOONSHOT_API_KEY:
113113
required: false
114+
ANTHROPIC_API_KEY:
115+
required: false
114116

115117
env:
116118
TF_BACKEND_RG: ${{ secrets.TF_BACKEND_RG }}
@@ -208,6 +210,7 @@ jobs:
208210
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || '' }}
209211
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY || '' }}
210212
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY || '' }}
213+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY || '' }}
211214
run: |
212215
set -euo pipefail
213216
@@ -233,6 +236,9 @@ jobs:
233236
add_provider "openrouter" "OPENROUTER_API_KEY" "${OPENROUTER_API_KEY}"
234237
add_provider "fireworks" "FIREWORKS_API_KEY" "${FIREWORKS_API_KEY}"
235238
add_provider "moonshot" "MOONSHOT_API_KEY" "${MOONSHOT_API_KEY}"
239+
# ADR 09: env_name is LITELLM_ANTHROPIC_API_KEY (the var the LiteLLM
240+
# model_list reads), sourced from the ANTHROPIC_API_KEY secret.
241+
add_provider "anthropic" "LITELLM_ANTHROPIC_API_KEY" "${ANTHROPIC_API_KEY}"
236242
237243
if [ "${providers}" != "{}" ]; then
238244
echo "TF_VAR_extra_providers=${providers}" >> "$GITHUB_ENV"

.github/workflows/deploy.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ jobs:
168168
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || '' }}
169169
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY || '' }}
170170
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY || '' }}
171+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY || '' }}
171172
run: |
172173
set -euo pipefail
173174
@@ -193,6 +194,9 @@ jobs:
193194
add_provider "openrouter" "OPENROUTER_API_KEY" "${OPENROUTER_API_KEY}"
194195
add_provider "fireworks" "FIREWORKS_API_KEY" "${FIREWORKS_API_KEY}"
195196
add_provider "moonshot" "MOONSHOT_API_KEY" "${MOONSHOT_API_KEY}"
197+
# ADR 09: env_name is LITELLM_ANTHROPIC_API_KEY (the var the LiteLLM
198+
# model_list reads), sourced from the ANTHROPIC_API_KEY secret.
199+
add_provider "anthropic" "LITELLM_ANTHROPIC_API_KEY" "${ANTHROPIC_API_KEY}"
196200
197201
if [ "${providers}" != "{}" ]; then
198202
echo "TF_VAR_extra_providers=${providers}" >> "$GITHUB_ENV"
@@ -363,3 +367,4 @@ jobs:
363367
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
364368
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
365369
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
370+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

docs/architecture/09-claude-code-via-sluice.md

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
# Routing Claude Code CLI Through Sluice
22

3-
Status: Open — awaiting decision
4-
Date: 2026-04-30
3+
Status: Accepted — 2026-07-03
4+
Date: 2026-04-30 (analysis) · 2026-07-03 (decision)
5+
6+
## Decision
7+
8+
CI and other **non-interactive** agents route Claude Code (and any Anthropic-SDK
9+
caller) through Sluice on API-key billing, requesting the **`agent-default`**
10+
alias. Sluice resolves `agent-default` to a concrete Claude model (currently
11+
`claude-haiku-4-5`, degrading to `claude-sonnet-4-6` then Azure `premium`), so
12+
the model can be re-routed centrally without touching any CI workflow.
13+
**Interactive** Claude Code use backed by a Claude Max subscription stays on
14+
**direct-Anthropic** auth — the flat-fee Max economics dominate per-token API
15+
rates for human-driven sessions (see the analysis and recommendation below,
16+
which this decision adopts unchanged).
17+
18+
Implemented via: an `anthropic` entry in the LiteLLM `model_list`
19+
(`infra/modules/sluice_aca/main.tf`), the `LITELLM_ANTHROPIC_API_KEY` secret
20+
wired through the standard `extra_providers` path, provider activation in the
21+
deploy workflows (`ANTHROPIC_API_KEY` GitHub secret → `TF_VAR_extra_providers`),
22+
and a dedicated `mystira-ci` virtual key allow-listing `agent-default`
23+
(`scripts/keys.yaml`).
524

625
## Question
726

@@ -109,30 +128,43 @@ spending subscription quota through a self-hosted gateway.
109128
on subscription-first (Claude Code in particular). Routing through API
110129
could miss features for weeks.
111130

112-
## If we proceed — what setup looks like
113-
114-
### Operator (one-time)
115-
116-
1. Generate an Anthropic Console API key (pay-per-token billing on the
117-
org's Console account)
118-
2. Store it in KV: `az keyvault secret set --vault-name pvc-prod-sluice-kv
119-
--name anthropic-api-key --value <key>`
120-
3. Add a `model_list` entry to the LiteLLM YAML in
121-
`infra/modules/sluice_aca/main.tf` — e.g.
122-
```yaml
123-
- model_name: claude-sonnet-4-6
124-
litellm_params:
125-
model: anthropic/claude-sonnet-4-6
126-
api_key: os.environ/LITELLM_ANTHROPIC_API_KEY
127-
model_info:
128-
input_cost_per_token: 0.000003
129-
output_cost_per_token: 0.000015
130-
```
131-
4. Mount `LITELLM_ANTHROPIC_API_KEY` from the `anthropic-api-key` KV
132-
secret on the LiteLLM container
133-
5. Update each consumer's `keys.yaml` entry to include the new Claude
134-
model in `models: [...]`
135-
6. `python scripts/manage_keys.py apply` — re-applies model allowlists
131+
## Setup (implemented)
132+
133+
Steps 2–5 of the original analysis are now **in code** — the `model_list`
134+
entries, the `LITELLM_ANTHROPIC_API_KEY` env wiring (via the standard
135+
`extra_providers` path), provider activation in the deploy workflows, and the
136+
`mystira-ci` allow-list all ship in this repo. The remaining operator work is
137+
placing the key and running one deploy.
138+
139+
### Operator (one-time — the single manual step to go live)
140+
141+
1. Generate an Anthropic Console API key (pay-per-token billing on the org's
142+
Console account).
143+
2. Make the key available to Terraform. Two equivalent options:
144+
- **Preferred — GitHub environment secret** (matches how every other
145+
provider key is supplied): set `ANTHROPIC_API_KEY` as a `prod` environment
146+
secret. The `Configure optional model providers` step folds it into
147+
`TF_VAR_extra_providers` as
148+
`{ anthropic = { api_key = <key>, env_name = "LITELLM_ANTHROPIC_API_KEY" } }`,
149+
and Terraform then **creates** the KV secret `provider-anthropic-key` and
150+
mounts it as the `LITELLM_ANTHROPIC_API_KEY` env var on the LiteLLM
151+
container.
152+
- **Direct KV** (matches the runbook convention below):
153+
```
154+
az keyvault secret set --vault-name pvc-prod-sluice-kv \
155+
--name anthropic-api-key --value <key>
156+
```
157+
Use this only if wiring `TF_VAR_extra_providers` from that KV secret rather
158+
than a GitHub secret; the `extra_providers`-managed secret is
159+
`provider-anthropic-key`, so keep the two out of sync deliberately.
160+
3. Deploy: run the `deploy` workflow (or merge to trigger it) so Terraform
161+
provisions `provider-anthropic-key`, injects the env var, and renders the
162+
Claude models + `agent-default` alias into the LiteLLM config.
163+
4. `python scripts/manage_keys.py apply` — applies the `mystira-ci` allow-list
164+
(and any other `keys.yaml` changes) on the running proxy.
165+
166+
CI consumers then set the two env vars from the per-consumer snippet below,
167+
using `vkey-mystira-ci`, and request `model="agent-default"`.
136168
137169
### Per-consumer (each service / user)
138170

infra/env/prod/terraform.tfvars

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,7 @@ enable_litellm_db = true
6464
# fireworks = { api_key = "...", env_name = "FIREWORKS_API_KEY" }
6565
# # Phase 3 — Kimi K2 direct Moonshot path (optional; Groq path uses GROQ_API_KEY above)
6666
# moonshot = { api_key = "sk-...", env_name = "MOONSHOT_API_KEY" }
67+
# # ADR 09 — Claude Code CLI / CI agents via Sluice (env_name is the var the
68+
# # LiteLLM model_list reads, NOT the GitHub secret name).
69+
# anthropic = { api_key = "sk-ant-...", env_name = "LITELLM_ANTHROPIC_API_KEY" }
6770
# }

infra/modules/sluice_aca/main.tf

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,72 @@ locals {
352352
},
353353
] : []
354354

355+
# ── Anthropic / Claude models (ADR 09) ─────────────────────────────────
356+
# Enables Claude Code CLI and other Anthropic-SDK callers to route through
357+
# Sluice on API-key billing (ADR 09: "API-key billing = through Sluice").
358+
# The `agent-default` alias is what non-interactive CI agents request; it
359+
# maps to the cheap Claude tier as primary (order 1) and degrades to the
360+
# mid tier via the fallback chain, so Sluice picks the concrete model and
361+
# can re-route later without touching CI workflows. Activated by supplying
362+
# an "anthropic" entry in extra_providers (env_name = LITELLM_ANTHROPIC_API_KEY).
363+
has_anthropic = contains(local.active_provider_names, "anthropic")
364+
anthropic_models = local.has_anthropic ? [
365+
# Concrete Claude models (current Anthropic model ids).
366+
# Cheap tier — Claude Haiku 4.5 ($1 / $5 per 1M tokens).
367+
{
368+
model_name = "claude-haiku-4-5"
369+
litellm_params = {
370+
model = "anthropic/claude-haiku-4-5"
371+
api_key = "os.environ/LITELLM_ANTHROPIC_API_KEY"
372+
}
373+
model_info = {
374+
input_cost_per_token = 0.000001
375+
output_cost_per_token = 0.000005
376+
}
377+
},
378+
# Mid tier — Claude Sonnet 4.6 ($3 / $15 per 1M tokens).
379+
{
380+
model_name = "claude-sonnet-4-6"
381+
litellm_params = {
382+
model = "anthropic/claude-sonnet-4-6"
383+
api_key = "os.environ/LITELLM_ANTHROPIC_API_KEY"
384+
}
385+
model_info = {
386+
input_cost_per_token = 0.000003
387+
output_cost_per_token = 0.000015
388+
}
389+
},
390+
# agent-default group — CI agents request model="agent-default".
391+
# Cheap Claude tier is primary (order 1) so Sluice picks the concrete
392+
# model; degrades to the mid tier (order 2) then to `premium` (Azure,
393+
# always present) via the fallback chain below.
394+
{
395+
model_name = "agent-default"
396+
litellm_params = {
397+
model = "anthropic/claude-haiku-4-5"
398+
api_key = "os.environ/LITELLM_ANTHROPIC_API_KEY"
399+
order = 1
400+
}
401+
model_info = {
402+
input_cost_per_token = 0.000001
403+
output_cost_per_token = 0.000005
404+
}
405+
},
406+
# agent-default fallback member — mid Claude tier (order 2).
407+
{
408+
model_name = "agent-default"
409+
litellm_params = {
410+
model = "anthropic/claude-sonnet-4-6"
411+
api_key = "os.environ/LITELLM_ANTHROPIC_API_KEY"
412+
order = 2
413+
}
414+
model_info = {
415+
input_cost_per_token = 0.000003
416+
output_cost_per_token = 0.000015
417+
}
418+
},
419+
] : []
420+
355421
# ── "auto" alias — points to cheapest entry with fallback chain ────────
356422
# auto is a model group with a single entry (Groq gpt-oss-20b if available,
357423
# otherwise Together, otherwise Fireworks). Fallback chains handle the rest.
@@ -407,9 +473,28 @@ locals {
407473
local.gemini_models,
408474
local.openrouter_models,
409475
local.moonshot_models,
476+
local.anthropic_models,
410477
local.auto_models,
411478
)
412479

480+
# ── Fallback chains ────────────────────────────────────────────────────
481+
# auto/cheap-* chains exist only with an extra provider (ADR 11); the
482+
# agent-default chain exists only with Anthropic (ADR 09). Built
483+
# separately and concatenated so either can appear on its own.
484+
# Each entry is a single-key map(list(string)); tomap() keeps the element
485+
# type uniform so concat() doesn't choke on differing object attribute sets.
486+
extra_fallbacks = local.has_any_extra ? [
487+
tomap({ auto = ["cheap-fast", "cheap-reasoning", "cheap-long-context", "premium"] }),
488+
tomap({ cheap-fast = ["cheap-reasoning", "premium"] }),
489+
tomap({ cheap-reasoning = ["premium"] }),
490+
tomap({ cheap-long-context = ["premium"] }),
491+
] : []
492+
# agent-default degrades cheap Claude → mid Claude → premium (Azure).
493+
anthropic_fallbacks = local.has_anthropic ? [
494+
tomap({ "agent-default" = ["claude-sonnet-4-6", "premium"] }),
495+
] : []
496+
combined_fallbacks = concat(local.extra_fallbacks, local.anthropic_fallbacks)
497+
413498
# Build with nullable optional keys, then filter out the nulls. Terraform's
414499
# ternary requires both branches to share a type, so the cleaner-looking
415500
# `condition ? {extras} : {}` doesn't compile against typed objects.
@@ -442,14 +527,11 @@ locals {
442527
budget_duration = var.budget_duration != "" ? var.budget_duration : null
443528
rpm_limit = var.rpm_limit > 0 ? var.rpm_limit : null
444529
tpm_limit = var.tpm_limit > 0 ? var.tpm_limit : null
445-
# Multi-provider fallback chains (ADR 11). Only added when at least one
446-
# extra provider is active; otherwise LiteLLM runs with Azure-only.
447-
fallbacks = local.has_any_extra ? [
448-
{ auto = ["cheap-fast", "cheap-reasoning", "cheap-long-context", "premium"] },
449-
{ cheap-fast = ["cheap-reasoning", "premium"] },
450-
{ cheap-reasoning = ["premium"] },
451-
{ cheap-long-context = ["premium"] },
452-
] : null
530+
# Multi-provider fallback chains (ADR 11 + ADR 09). The auto/cheap-*
531+
# chains are added when an extra provider is active; the agent-default
532+
# chain is added when Anthropic is active. Concatenated so either set can
533+
# appear independently; null only when neither is present (Azure-only).
534+
fallbacks = length(local.combined_fallbacks) > 0 ? local.combined_fallbacks : null
453535
context_window_fallbacks = local.has_any_extra ? [
454536
{ cheap-fast = ["cheap-long-context"] },
455537
{ cheap-reasoning = ["cheap-long-context"] },

scripts/keys.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ keys:
6767
operator: jurie@phoenixvc.tech
6868
use: interactive
6969

70+
# ── Mystira CI (non-interactive agents via Sluice — ADR 09) ──────────────
71+
# Claude Code CLI in CI / automation routes through Sluice on API-key
72+
# billing. Requests model="agent-default"; Sluice maps it to the cheap
73+
# Claude tier and can re-route later without a workflow change. Separate
74+
# from claude-code-cli (interactive, Max-subscription — stays direct).
75+
- alias: mystira-ci
76+
max_budget: 10
77+
budget_duration: 7d
78+
rpm_limit: 120
79+
tpm_limit: 200000
80+
models: [agent-default, claude-haiku-4-5, claude-sonnet-4-6, gpt-4o, text-embedding-3-large, auto, cheap-fast, cheap-reasoning, premium]
81+
metadata:
82+
project: mystira-workspace
83+
service: ci
84+
use: non-interactive
85+
contact: jurie@phoenixvc.tech
86+
7087
# ── Dashboard Test Playground (small, public-key style) ──────────────────
7188
- alias: dashboard-playground
7289
max_budget: 1

0 commit comments

Comments
 (0)