new endpoint GET /api/v1/gateways/{gateway_id}/models
This commit is contained in:
parent
809975cb76
commit
240313a431
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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{" "}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue