Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cd87adf
Initialize SDLC contract for issue #2769
May 22, 2026
9dc9033
refine(#2769): analysis for non-Claude models via LiteLLM proxy
May 22, 2026
1c13e49
Persist agent statefile writes before refine sync
May 22, 2026
c066022
Persist statefiles after refine phase
May 22, 2026
12fa8d2
Persist HITL resolution after refine phase gate
May 22, 2026
851ef43
plan(#2769): architecture analysis for non-Claude models via LiteLLM …
May 22, 2026
5ef34fb
plan: decompose #2769 into gateway router + per-agent model config sl…
May 22, 2026
dfe5ffb
plan(#2769): risk assessment for LiteLLM proxy integration
May 22, 2026
fce4001
plan(#2769) v2: pin Semantics B for LiteLLM body rewrite, address rev…
May 22, 2026
4104cc5
Persist statefiles after plan phase
May 22, 2026
8c68062
docs(#2769): document UpstreamRegistry seam and LiteLLM topology
May 22, 2026
3ad6069
gateway: add UpstreamRegistry seam + LiteLLM topology (no-op default)…
May 22, 2026
b79f927
gateway: format Invalid-upstream error message per ruff format
May 22, 2026
f5076a8
build: extend k3s-secrets to surface LITELLM_MASTER_KEY as a discrete…
May 22, 2026
b68f570
tests(#2769 slice-1): scaffold UpstreamRegistry + session-upstream tests
May 22, 2026
bf21044
tests(#2769 slice-1): finalize tester suite against coder v1 (commit …
May 22, 2026
bf857bf
tests(#2769 slice-1): drop unused type:ignore[import-not-found] on up…
May 22, 2026
e36c092
Fix llm-api-calls lint: suppress EGG200 in upstream_registry
james-in-a-box[bot] May 22, 2026
570b72f
Fix checks: apply automated formatting fixes
May 22, 2026
b914607
Address review feedback on #2769 slice-1 gateway upstream router
egg-reviewer[bot] May 22, 2026
020569d
Address re-review suggestions on #2769 slice-1 upstream router
egg-reviewer[bot] May 22, 2026
1ff4803
[issue-2769][merge-gate] Add per-agent non-Claude model support via..…
james-in-a-box[bot] May 22, 2026
f179482
Merge remote-tracking branch 'origin/main' into egg/issue-2769/slice-1
jwbron May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 488 additions & 0 deletions .egg-state/agent-outputs/2769-architect-output.json

Large diffs are not rendered by default.

459 changes: 459 additions & 0 deletions .egg-state/agent-outputs/2769-risk_analyst-output.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -504,10 +504,20 @@ k3s-secrets: ## Create gateway secrets from ~/.config/egg/
fi
@echo "==> Creating gateway-secrets in egg-system namespace..."
@echo " (all files under ~/.config/egg/ become keys in the secret)"
@# LiteLLM master key (issue #2769): the in-cluster LiteLLM
@# Deployment expects ``gateway-secrets.litellm-master-key`` so the
@# gateway's injected x-api-key matches LiteLLM's master_key. The
@# value lives in ``secrets.env`` as ``LITELLM_MASTER_KEY=...``;
@# extract it and surface it as a discrete literal key so both
@# sides of the wire share one source of truth. Empty value is the
@# no-op default (the manifest reads the Secret with
@# ``optional: true``).
@LITELLM_KEY="$$(grep -E '^[[:space:]]*LITELLM_MASTER_KEY[[:space:]]*=' "$$HOME/.config/egg/secrets.env" 2>/dev/null | tail -n1 | cut -d= -f2- | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$$//' -e 's/^"//' -e 's/"$$//' -e "s/^'//" -e "s/'$$//")"; \
export KUBECONFIG=$${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml} && \
kubectl apply -f k8s/base/namespaces.yaml && \
kubectl -n egg-system create secret generic gateway-secrets \
--from-file=$$HOME/.config/egg/ \
--from-literal=litellm-master-key="$$LITELLM_KEY" \
--dry-run=client -o yaml | kubectl apply -f -

deploy: k3s-secrets ## Deploy egg to k3s
Expand Down
50 changes: 50 additions & 0 deletions config/repo_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,56 @@ def should_disable_auto_fix(repo: str) -> bool:
return cast(bool, get_repo_setting(repo, "disable_auto_fix", False))


def get_default_agent_model(repo: str) -> str | None:
"""Return the repository-level default agent model, or ``None`` when unset.

This is the second tier of the per-agent model resolution precedence
(see ``orchestrator/agent_model_resolution.py``):

1. ``PipelineConfig.agent_models[role]`` (per-pipeline override)
2. ``repositories.yaml`` ``default_agent_model`` (this helper)
3. Built-in ``"opus"`` default

The value follows the same classifier as ``agent_models``: a recognised
Claude alias (``opus``, ``opus[1m]``, ``sonnet``, ``sonnet[1m]``,
``haiku``, ``claude-*``) routes through the Anthropic upstream, anything
else routes through the in-cluster LiteLLM proxy with the alias
``"opus"`` presented to Claude Code (cq-5 mitigation).

Args:
repo: Repository in "owner/repo" format

Returns:
The configured model string, or ``None`` when the repo has no
per-repo entry, the entry omits ``default_agent_model``, or the
``repositories.yaml`` file is absent (a missing config file is the
same observable as a missing entry — preserves the
no-op-by-default invariant for callers like ``resolve_agent_model``
that run inside spawn paths where the config file may not be
present, e.g. unit tests and ephemeral CI environments).

Raises:
ValueError: When ``default_agent_model`` is set to a non-string
YAML value (e.g. ``default_agent_model: 4``). Surfacing the
misconfiguration loudly here keeps it out of ``classify_model``,
where a non-string would otherwise raise an opaque ``TypeError``
from the regex internals.
"""
try:
value = get_repo_setting(repo, "default_agent_model", None)
except FileNotFoundError:
return None
if value is None:
return None
if not isinstance(value, str):
raise ValueError(
f"default_agent_model for {repo!r} must be a string, got "
f"{type(value).__name__}: {value!r}. Set it to a recognised "
f"Claude alias (opus, sonnet, …) or a LiteLLM model name."
)
return value


try:
from egg_config.validators import validate_checks
except ImportError:
Expand Down
18 changes: 18 additions & 0 deletions config/repositories.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ readable_repos:
# **/*_test.go in tests_globs). Security-relevant blocklists
# (.egg-state/contracts/, .github/) are hard-coded and cannot be
# relaxed. See docs/guides/sdlc-pipeline.md#per-repository-role-patterns.
# - default_agent_model: Repository-level default for the per-agent
# model knob added in #2769. Used by every agent role unless the
# pipeline submission overrides it via ``agent_models``. A recognised
# Claude alias (opus, opus[1m], sonnet, sonnet[1m], haiku, claude-*)
# routes through the Anthropic upstream; anything else routes through the
# in-cluster LiteLLM proxy with the recognised alias "opus" presented
# to Claude Code (cq-5 mitigation). Precedence:
# PipelineConfig.agent_models[role]
# > this default_agent_model
# > built-in "opus" default
# Default: not set (every role runs on built-in "opus").
repo_settings:
# Example:
# YOUR_USERNAME/egg:
Expand Down Expand Up @@ -190,6 +201,13 @@ repo_settings:
# tests_globs: ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"]
# code_globs: ["**/*.ts", "**/*.tsx", "**/*.js"]
# docs_globs: ["**/*.md", "docs/"]
#
# # Per-agent model example (#2769): route every role on this repo through
# # the LiteLLM proxy by default, picking up the hosted-Qwen model entry
# # populated in the LiteLLM ConfigMap. The pipeline-level ``agent_models``
# # field still wins when set.
# YOUR_USERNAME/qwen-pilot:
# default_agent_model: qwen3-coder-30b

# User mode configuration (optional)
# When auth_mode is set to "user" for a repo, operations will be
Expand Down
18 changes: 18 additions & 0 deletions config/secrets.template.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@
ANTHROPIC_API_KEY=""
ANTHROPIC_OAUTH_TOKEN=""

# =============================================================================
# LiteLLM Proxy (Optional — non-Claude model routing, issue #2769)
# =============================================================================
# Master key the gateway injects as ``x-api-key`` on every request routed
# through the in-cluster LiteLLM proxy. The same value is consumed by the
# LiteLLM Deployment via the ``gateway-secrets`` Secret (key
# ``litellm-master-key``) so both sides of the wire agree.
#
# Per-backend provider credentials (e.g. TOGETHER_API_KEY for a hosted
# Qwen provider — see cq-6) are NOT stored here; they go in LiteLLM's
# own env-var slots so the gateway never sees the raw provider key.
#
# Leave empty to disable LiteLLM routing — no agent will be routed to
# LiteLLM with this unset, regardless of any per-pipeline
# ``agent_models`` override. The Claude path is byte-identical.

LITELLM_MASTER_KEY=""

# =============================================================================
# Slack Integration
# =============================================================================
Expand Down
1 change: 1 addition & 0 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ The SDLC pipeline orchestrates agent-based development with structurally enforce
- [Gateway Auto-Filter](gateway-auto-filter.md) - Restricted-path rejection on push and the commit-authorship registry that backs attribution
- [Credential Injection](credential-injection.md) - Zero-credential sandbox with API key proxy
- [Network Isolation](network-isolation.md) - Public/private network modes
- [Upstream Routing](upstream-routing.md) - `UpstreamRegistry` seam, LiteLLM topology, per-session routing decision, and no-op-by-default invariant for non-Claude agent backends ([#2769](https://github.com/jwbron/egg/issues/2769))
- [SDLC Pipeline](sdlc-pipeline.md) - Structurally enforced agent checkpoints
- [Declarative Setup](declarative-setup.md) - Python-based setup
- [Logging](logging.md) - Structured JSON logging
Expand Down
19 changes: 19 additions & 0 deletions docs/architecture/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ See `plugins/refine-plan/skills/refine-plan/agents/applier.md`'s "Out of scope:
- Orchestrator-side drain hook: `orchestrator/routes/pipelines.py::_drain_wontdo_batch_after_apply` — invoked from both the auto-advance and HITL-resolution apply-phase exit paths; writes per-Task `jira_action_status` back via the `on_entry_result` callback.
- Issue-level decision record: [#1557 decision-15](https://github.com/jwbron/egg/issues/1557) (trust-boundary for Jira transitions).

## Upstream Routing (Per-Agent Model Backends, [#2769](https://github.com/jwbron/egg/issues/2769))

Per-agent `/v1/messages` traffic routes through an `UpstreamRegistry`
in the gateway that resolves the upstream (Anthropic vs LiteLLM) from
per-session metadata declared by the orchestrator at session-create
time — the same IP-keyed session lookup that already drives
`session_mode`. Slice 1 of #2769 lands the router and the LiteLLM
Deployment + Service in `egg-system`, no-op by default; slice 2 adds
`PipelineConfig.agent_models` and a repository-level
`default_agent_model` for the orchestrator-side resolution. Until an
operator opts in, every existing pipeline keeps running on Claude
with byte-identical gateway behavior.

See [Upstream Routing](upstream-routing.md) for the gateway-side
seam (registry, credential layout, request lifecycle, failure
policy, and the no-op-by-default invariant). The operator-facing
setup ships in slice 2 as `docs/guides/per-agent-models.md` (that
file does not exist until slice 2 lands).

## Network Mode

Pipelines can specify an explicit network mode that controls internet access for spawned containers:
Expand Down
Loading
Loading