diff --git a/components/backend/handlers/projects.go b/components/backend/handlers/projects.go index dd6891390..cb56c5f9d 100644 --- a/components/backend/handlers/projects.go +++ b/components/backend/handlers/projects.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "encoding/json" "fmt" "log" "net/http" @@ -137,9 +138,20 @@ func GetClusterInfo(c *gin.Context) { isOpenShift := isOpenShiftCluster() vertexEnabled := os.Getenv("CLAUDE_CODE_USE_VERTEX") == "1" + var models []map[string]interface{} + if raw := os.Getenv("MODELS_JSON"); raw != "" { + if err := json.Unmarshal([]byte(raw), &models); err != nil { + log.Printf("Warning: failed to parse MODELS_JSON: %v", err) + } + } + if models == nil { + models = []map[string]interface{}{} + } + c.JSON(http.StatusOK, gin.H{ "isOpenShift": isOpenShift, "vertexEnabled": vertexEnabled, + "models": models, }) } diff --git a/components/frontend/src/components/create-session-dialog.tsx b/components/frontend/src/components/create-session-dialog.tsx index f25ec3770..c5ea4f900 100644 --- a/components/frontend/src/components/create-session-dialog.tsx +++ b/components/frontend/src/components/create-session-dialog.tsx @@ -35,9 +35,10 @@ import { import type { CreateAgenticSessionRequest } from "@/types/agentic-session"; import { useCreateSession } from "@/services/queries/use-sessions"; import { useIntegrationsStatus } from "@/services/queries/use-integrations"; +import { useClusterInfo } from "@/hooks/use-cluster-info"; import { errorToast } from "@/hooks/use-toast"; -const models = [ +const fallbackModels = [ { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" }, { value: "claude-opus-4-6", label: "Claude Opus 4.6" }, { value: "claude-opus-4-5", label: "Claude Opus 4.5" }, @@ -69,6 +70,13 @@ export function CreateSessionDialog({ const [open, setOpen] = useState(false); const router = useRouter(); const createSessionMutation = useCreateSession(); + const { models: clusterModels } = useClusterInfo(); + + const models = clusterModels.length > 0 + ? clusterModels.map((m) => ({ value: m.name, label: m.displayName })) + : fallbackModels; + + const defaultModel = clusterModels.find((m) => m.default)?.name ?? "claude-sonnet-4-5"; const { data: integrationsStatus } = useIntegrationsStatus(); @@ -81,7 +89,7 @@ export function CreateSessionDialog({ resolver: zodResolver(formSchema), defaultValues: { displayName: "", - model: "claude-sonnet-4-5", + model: defaultModel, temperature: 0.7, maxTokens: 4000, timeout: 300, diff --git a/components/frontend/src/hooks/use-cluster-info.ts b/components/frontend/src/hooks/use-cluster-info.ts index f35d82fea..c13eab3b8 100644 --- a/components/frontend/src/hooks/use-cluster-info.ts +++ b/components/frontend/src/hooks/use-cluster-info.ts @@ -4,19 +4,21 @@ */ import { useClusterInfo as useClusterInfoQuery } from '@/services/queries/use-cluster'; +import type { ModelInfo } from '@/services/api/cluster'; export type ClusterInfo = { isOpenShift: boolean; vertexEnabled: boolean; + models: ModelInfo[]; isLoading: boolean; isError: boolean; }; /** - * Detects whether the cluster is OpenShift or vanilla Kubernetes - * and whether Vertex AI is enabled - * Calls the /api/cluster-info endpoint which checks for project.openshift.io API group - * and CLAUDE_CODE_USE_VERTEX environment variable + * Detects whether the cluster is OpenShift or vanilla Kubernetes, + * whether Vertex AI is enabled, and available models + * Calls the /api/cluster-info endpoint which checks for project.openshift.io API group, + * CLAUDE_CODE_USE_VERTEX environment variable, and MODELS_JSON ConfigMap */ export function useClusterInfo(): ClusterInfo { const { data, isLoading, isError } = useClusterInfoQuery(); @@ -24,6 +26,7 @@ export function useClusterInfo(): ClusterInfo { return { isOpenShift: data?.isOpenShift ?? false, vertexEnabled: data?.vertexEnabled ?? false, + models: data?.models ?? [], isLoading, isError, }; diff --git a/components/frontend/src/services/api/cluster.ts b/components/frontend/src/services/api/cluster.ts index 8e3157cd1..0d4782e27 100644 --- a/components/frontend/src/services/api/cluster.ts +++ b/components/frontend/src/services/api/cluster.ts @@ -5,9 +5,17 @@ import { apiClient } from './client'; +export type ModelInfo = { + name: string; + displayName: string; + vertexId?: string; + default?: boolean; +}; + export type ClusterInfo = { isOpenShift: boolean; vertexEnabled: boolean; + models: ModelInfo[]; }; /** diff --git a/components/manifests/base/backend-deployment.yaml b/components/manifests/base/backend-deployment.yaml index b9414bdbf..9afee6b49 100644 --- a/components/manifests/base/backend-deployment.yaml +++ b/components/manifests/base/backend-deployment.yaml @@ -131,6 +131,12 @@ spec: configMapKeyRef: name: operator-config key: GOOGLE_APPLICATION_CREDENTIALS + - name: MODELS_JSON + valueFrom: + configMapKeyRef: + name: operator-config + key: MODELS_JSON + optional: true resources: requests: cpu: 100m diff --git a/components/manifests/base/operator-deployment.yaml b/components/manifests/base/operator-deployment.yaml index fe6a7b08e..4c48fbfdd 100644 --- a/components/manifests/base/operator-deployment.yaml +++ b/components/manifests/base/operator-deployment.yaml @@ -71,6 +71,12 @@ spec: configMapKeyRef: name: operator-config key: GOOGLE_APPLICATION_CREDENTIALS + - name: MODELS_JSON + valueFrom: + configMapKeyRef: + name: operator-config + key: MODELS_JSON + optional: true # Platform-wide Langfuse observability configuration # All LANGFUSE_* config stored in ambient-admin-langfuse-secret (platform-admin managed) - name: LANGFUSE_ENABLED diff --git a/components/manifests/minikube/operator-config.yaml b/components/manifests/minikube/operator-config.yaml index 21904a419..37eda94d2 100644 --- a/components/manifests/minikube/operator-config.yaml +++ b/components/manifests/minikube/operator-config.yaml @@ -26,4 +26,6 @@ data: CLOUD_ML_REGION: "global" ANTHROPIC_VERTEX_PROJECT_ID: "" GOOGLE_APPLICATION_CREDENTIALS: "/app/vertex/ambient-code-key.json" + # Available models for the platform (consumed by backend, operator, runner) + MODELS_JSON: '[{"name":"claude-sonnet-4-5","displayName":"Claude Sonnet 4.5","vertexId":"claude-sonnet-4-5@20250929","default":true},{"name":"claude-opus-4-6","displayName":"Claude Opus 4.6","vertexId":"claude-opus-4-6@20260115"},{"name":"claude-opus-4-5","displayName":"Claude Opus 4.5","vertexId":"claude-opus-4-5@20251101"},{"name":"claude-opus-4-1","displayName":"Claude Opus 4.1","vertexId":"claude-opus-4-1@20250805"},{"name":"claude-haiku-4-5","displayName":"Claude Haiku 4.5","vertexId":"claude-haiku-4-5@20251001"}]' diff --git a/components/manifests/overlays/e2e/operator-config.yaml b/components/manifests/overlays/e2e/operator-config.yaml index e0158c20f..2b18fd57d 100644 --- a/components/manifests/overlays/e2e/operator-config.yaml +++ b/components/manifests/overlays/e2e/operator-config.yaml @@ -13,3 +13,5 @@ data: CLOUD_ML_REGION: "" ANTHROPIC_VERTEX_PROJECT_ID: "" GOOGLE_APPLICATION_CREDENTIALS: "" + # Available models for the platform (consumed by backend, operator, runner) + MODELS_JSON: '[{"name":"claude-sonnet-4-5","displayName":"Claude Sonnet 4.5","vertexId":"claude-sonnet-4-5@20250929","default":true},{"name":"claude-opus-4-6","displayName":"Claude Opus 4.6","vertexId":"claude-opus-4-6@20260115"},{"name":"claude-opus-4-5","displayName":"Claude Opus 4.5","vertexId":"claude-opus-4-5@20251101"},{"name":"claude-opus-4-1","displayName":"Claude Opus 4.1","vertexId":"claude-opus-4-1@20250805"},{"name":"claude-haiku-4-5","displayName":"Claude Haiku 4.5","vertexId":"claude-haiku-4-5@20251001"}]' diff --git a/components/manifests/overlays/kind/operator-config.yaml b/components/manifests/overlays/kind/operator-config.yaml index e0158c20f..2b18fd57d 100644 --- a/components/manifests/overlays/kind/operator-config.yaml +++ b/components/manifests/overlays/kind/operator-config.yaml @@ -13,3 +13,5 @@ data: CLOUD_ML_REGION: "" ANTHROPIC_VERTEX_PROJECT_ID: "" GOOGLE_APPLICATION_CREDENTIALS: "" + # Available models for the platform (consumed by backend, operator, runner) + MODELS_JSON: '[{"name":"claude-sonnet-4-5","displayName":"Claude Sonnet 4.5","vertexId":"claude-sonnet-4-5@20250929","default":true},{"name":"claude-opus-4-6","displayName":"Claude Opus 4.6","vertexId":"claude-opus-4-6@20260115"},{"name":"claude-opus-4-5","displayName":"Claude Opus 4.5","vertexId":"claude-opus-4-5@20251101"},{"name":"claude-opus-4-1","displayName":"Claude Opus 4.1","vertexId":"claude-opus-4-1@20250805"},{"name":"claude-haiku-4-5","displayName":"Claude Haiku 4.5","vertexId":"claude-haiku-4-5@20251001"}]' diff --git a/components/manifests/overlays/local-dev/operator-config-crc.yaml b/components/manifests/overlays/local-dev/operator-config-crc.yaml index b6b368b9a..3a1c3ace6 100644 --- a/components/manifests/overlays/local-dev/operator-config-crc.yaml +++ b/components/manifests/overlays/local-dev/operator-config-crc.yaml @@ -11,3 +11,5 @@ data: CLOUD_ML_REGION: "" ANTHROPIC_VERTEX_PROJECT_ID: "" GOOGLE_APPLICATION_CREDENTIALS: "" + # Available models for the platform (consumed by backend, operator, runner) + MODELS_JSON: '[{"name":"claude-sonnet-4-5","displayName":"Claude Sonnet 4.5","vertexId":"claude-sonnet-4-5@20250929","default":true},{"name":"claude-opus-4-6","displayName":"Claude Opus 4.6","vertexId":"claude-opus-4-6@20260115"},{"name":"claude-opus-4-5","displayName":"Claude Opus 4.5","vertexId":"claude-opus-4-5@20251101"},{"name":"claude-opus-4-1","displayName":"Claude Opus 4.1","vertexId":"claude-opus-4-1@20250805"},{"name":"claude-haiku-4-5","displayName":"Claude Haiku 4.5","vertexId":"claude-haiku-4-5@20251001"}]' diff --git a/components/manifests/overlays/production/operator-config-openshift.yaml b/components/manifests/overlays/production/operator-config-openshift.yaml index 546d7325c..a6436a846 100644 --- a/components/manifests/overlays/production/operator-config-openshift.yaml +++ b/components/manifests/overlays/production/operator-config-openshift.yaml @@ -11,3 +11,5 @@ data: CLOUD_ML_REGION: "global" ANTHROPIC_VERTEX_PROJECT_ID: "ambient-code-platform" GOOGLE_APPLICATION_CREDENTIALS: "/app/vertex/ambient-code-key.json" + # Available models for the platform (consumed by backend, operator, runner) + MODELS_JSON: '[{"name":"claude-sonnet-4-5","displayName":"Claude Sonnet 4.5","vertexId":"claude-sonnet-4-5@20250929","default":true},{"name":"claude-opus-4-6","displayName":"Claude Opus 4.6","vertexId":"claude-opus-4-6@20260115"},{"name":"claude-opus-4-5","displayName":"Claude Opus 4.5","vertexId":"claude-opus-4-5@20251101"},{"name":"claude-opus-4-1","displayName":"Claude Opus 4.1","vertexId":"claude-opus-4-1@20250805"},{"name":"claude-haiku-4-5","displayName":"Claude Haiku 4.5","vertexId":"claude-haiku-4-5@20251001"}]' diff --git a/components/operator/internal/handlers/sessions.go b/components/operator/internal/handlers/sessions.go index 5183c13f6..2cd8f04b0 100644 --- a/components/operator/internal/handlers/sessions.go +++ b/components/operator/internal/handlers/sessions.go @@ -1001,6 +1001,11 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error { base = append(base, corev1.EnvVar{Name: "CLAUDE_CODE_USE_VERTEX", Value: "0"}) } + // Pass dynamic model configuration to runner + if modelsJSON := os.Getenv("MODELS_JSON"); modelsJSON != "" { + base = append(base, corev1.EnvVar{Name: "MODELS_JSON", Value: modelsJSON}) + } + // Add PARENT_SESSION_ID if this is a continuation if parentSessionID != "" { base = append(base, corev1.EnvVar{Name: "PARENT_SESSION_ID", Value: parentSessionID}) diff --git a/components/runners/claude-code-runner/auth.py b/components/runners/claude-code-runner/auth.py index 27d4cc77d..e040b1e5f 100644 --- a/components/runners/claude-code-runner/auth.py +++ b/components/runners/claude-code-runner/auth.py @@ -45,7 +45,9 @@ def sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: # --------------------------------------------------------------------------- # Anthropic API → Vertex AI model name mapping -VERTEX_MODEL_MAP: dict[str, str] = { +# Built from MODELS_JSON env var (set via operator-config ConfigMap) if available, +# otherwise falls back to hardcoded defaults. +_HARDCODED_VERTEX_MAP: dict[str, str] = { "claude-opus-4-5": "claude-opus-4-5@20251101", "claude-opus-4-1": "claude-opus-4-1@20250805", "claude-sonnet-4-5": "claude-sonnet-4-5@20250929", @@ -53,6 +55,21 @@ def sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: } +def _build_vertex_model_map() -> dict[str, str]: + raw = os.environ.get("MODELS_JSON", "") + if not raw: + return dict(_HARDCODED_VERTEX_MAP) + try: + models = _json.loads(raw) + return {m["name"]: m["vertexId"] for m in models if m.get("vertexId")} + except Exception: + logger.warning("Failed to parse MODELS_JSON, using hardcoded map") + return dict(_HARDCODED_VERTEX_MAP) + + +VERTEX_MODEL_MAP: dict[str, str] = _build_vertex_model_map() + + def map_to_vertex_model(model: str) -> str: """Map Anthropic API model names to Vertex AI model names.""" return VERTEX_MODEL_MAP.get(model, model)