diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 46eb812..00e61ca 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -509,6 +509,46 @@ async def get_gateway_cron( ) +@router.get( + "/{gateway_id}/models", + summary="List models available on the gateway", + description="Return the model IDs advertised by the OpenClaw gateway via models.list.", +) +async def get_gateway_models( + gateway_id: UUID, + session: AsyncSession = SESSION_DEP, + ctx: OrganizationContext = ORG_MEMBER_DEP, +) -> dict: + """Return available model IDs from the gateway.""" + from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig + + service = GatewayAdminLifecycleService(session) + gateway = await service.require_gateway( + gateway_id=gateway_id, + organization_id=ctx.organization.id, + ) + config = GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + ) + try: + raw = await openclaw_call("models.list", config=config) + except Exception: + raw = None + + if isinstance(raw, list): + models = [str(m) for m in raw if m] + elif isinstance(raw, dict): + nested = raw.get("models") or raw.get("items") or raw.get("data") or [] + models = [str(m) for m in nested if m] if isinstance(nested, list) else [] + else: + models = [] + + return {"gateway_id": str(gateway_id), "models": models} + + @router.get( "/{gateway_id}/health", response_model=SystemHealthResponse, diff --git a/frontend/src/components/git/AssignIssueAgentDialog.tsx b/frontend/src/components/git/AssignIssueAgentDialog.tsx index b7a31d5..b1a03b9 100644 --- a/frontend/src/components/git/AssignIssueAgentDialog.tsx +++ b/frontend/src/components/git/AssignIssueAgentDialog.tsx @@ -12,7 +12,7 @@ import { useListBoardsApiV1BoardsGet, type listBoardsApiV1BoardsGetResponse, } from "@/api/generated/boards/boards"; -import { ApiError } from "@/api/mutator"; +import { ApiError, customFetch } from "@/api/mutator"; import { Dialog, DialogContent, @@ -32,6 +32,18 @@ import { type LinkedBoard = { id: string; name: string }; +async function fetchGatewayModels(gatewayId: string): Promise { + try { + const res = await customFetch<{ data: { models: string[] }; status: number }>( + `/api/v1/gateways/${encodeURIComponent(gatewayId)}/models`, + { method: "GET" }, + ); + return res.status === 200 ? (res.data.models ?? []) : []; + } catch { + return []; + } +} + type AssignIssueAgentDialogProps = { issue: ForgejoIssue | null; repositoryName: string; @@ -75,6 +87,9 @@ export function AssignIssueAgentDialog({ const [boardId, setBoardId] = useState(""); const [agentId, setAgentId] = useState(""); const [priority, setPriority] = useState("medium"); + const [model, setModel] = useState(""); + const [availableModels, setAvailableModels] = useState([]); + const [modelsLoading, setModelsLoading] = useState(false); const [instructions, setInstructions] = useState(""); const [startImmediately, setStartImmediately] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); @@ -149,6 +164,9 @@ export function AssignIssueAgentDialog({ setBoardId(""); setAgentId(""); setPriority("medium"); + setModel(""); + setAvailableModels([]); + setModelsLoading(false); setInstructions(""); setStartImmediately(true); setError(null); @@ -165,13 +183,48 @@ export function AssignIssueAgentDialog({ useEffect(() => { setAgentId(""); + setAvailableModels([]); + setModel(""); }, [boardId]); + // When agent changes, fetch models from its gateway and pre-select the agent's default. + useEffect(() => { + if (!agentId) { + setAvailableModels([]); + setModel(""); + return; + } + const selectedAgent = agents.find((a) => a.id === agentId); + if (!selectedAgent) return; + + const agentDefaultModel = + typeof selectedAgent.identity_profile?.model === "string" + ? (selectedAgent.identity_profile.model as string) + : ""; + + let cancelled = false; + setModelsLoading(true); + fetchGatewayModels(selectedAgent.gateway_id).then((models) => { + if (cancelled) return; + setAvailableModels(models); + setModel(agentDefaultModel && models.includes(agentDefaultModel) ? agentDefaultModel : (models[0] ?? "")); + setModelsLoading(false); + }); + return () => { + cancelled = true; + }; + }, [agentId, agents]); + if (!issue) return null; const noLinkedBoards = linkedBoardsLoaded && !boardsLoading && linkedBoards.length === 0; - const prompt = instructions.trim(); + const prompt = [ + model ? `Model: ${model}` : "", + instructions.trim(), + ] + .filter(Boolean) + .join("\n\n"); const handleSubmit = async () => { if (!boardId || !agentId || !prompt) return; @@ -413,6 +466,30 @@ export function AssignIssueAgentDialog({ +
+ + +
+