new endpoint GET /api/v1/gateways/{gateway_id}/models

This commit is contained in:
null 2026-05-25 17:43:06 -05:00
parent 809975cb76
commit 240313a431
2 changed files with 119 additions and 2 deletions

View File

@ -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( @router.get(
"/{gateway_id}/health", "/{gateway_id}/health",
response_model=SystemHealthResponse, response_model=SystemHealthResponse,

View File

@ -12,7 +12,7 @@ import {
useListBoardsApiV1BoardsGet, useListBoardsApiV1BoardsGet,
type listBoardsApiV1BoardsGetResponse, type listBoardsApiV1BoardsGetResponse,
} from "@/api/generated/boards/boards"; } from "@/api/generated/boards/boards";
import { ApiError } from "@/api/mutator"; import { ApiError, customFetch } from "@/api/mutator";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -32,6 +32,18 @@ import {
type LinkedBoard = { id: string; name: string }; type LinkedBoard = { id: string; name: string };
async function fetchGatewayModels(gatewayId: string): Promise<string[]> {
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 = { type AssignIssueAgentDialogProps = {
issue: ForgejoIssue | null; issue: ForgejoIssue | null;
repositoryName: string; repositoryName: string;
@ -75,6 +87,9 @@ export function AssignIssueAgentDialog({
const [boardId, setBoardId] = useState(""); const [boardId, setBoardId] = useState("");
const [agentId, setAgentId] = useState(""); const [agentId, setAgentId] = useState("");
const [priority, setPriority] = useState("medium"); const [priority, setPriority] = useState("medium");
const [model, setModel] = useState("");
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [instructions, setInstructions] = useState(""); const [instructions, setInstructions] = useState("");
const [startImmediately, setStartImmediately] = useState(true); const [startImmediately, setStartImmediately] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -149,6 +164,9 @@ export function AssignIssueAgentDialog({
setBoardId(""); setBoardId("");
setAgentId(""); setAgentId("");
setPriority("medium"); setPriority("medium");
setModel("");
setAvailableModels([]);
setModelsLoading(false);
setInstructions(""); setInstructions("");
setStartImmediately(true); setStartImmediately(true);
setError(null); setError(null);
@ -165,13 +183,48 @@ export function AssignIssueAgentDialog({
useEffect(() => { useEffect(() => {
setAgentId(""); setAgentId("");
setAvailableModels([]);
setModel("");
}, [boardId]); }, [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; if (!issue) return null;
const noLinkedBoards = const noLinkedBoards =
linkedBoardsLoaded && !boardsLoading && linkedBoards.length === 0; linkedBoardsLoaded && !boardsLoading && linkedBoards.length === 0;
const prompt = instructions.trim(); const prompt = [
model ? `Model: ${model}` : "",
instructions.trim(),
]
.filter(Boolean)
.join("\n\n");
const handleSubmit = async () => { const handleSubmit = async () => {
if (!boardId || !agentId || !prompt) return; if (!boardId || !agentId || !prompt) return;
@ -413,6 +466,30 @@ export function AssignIssueAgentDialog({
</select> </select>
</div> </div>
<div className="space-y-1.5">
<label className="text-sm font-medium text-strong">
Model
</label>
<select
value={model}
onChange={(e) => setModel(e.target.value)}
disabled={isSubmitting || modelsLoading || !agentId}
className="w-full rounded-xl border border-[color:rgba(168,85,247,0.2)] bg-[color:var(--surface)] px-3 py-2 text-sm text-strong focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)] disabled:opacity-60"
>
{modelsLoading ? (
<option value="">Loading models</option>
) : availableModels.length === 0 ? (
<option value="">No models available</option>
) : (
availableModels.map((m) => (
<option key={m} value={m}>
{m}
</option>
))
)}
</select>
</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-sm font-medium text-strong"> <label className="text-sm font-medium text-strong">
Agent prompt{" "} Agent prompt{" "}