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(
|
||||
"/{gateway_id}/health",
|
||||
response_model=SystemHealthResponse,
|
||||
|
|
|
|||
|
|
@ -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<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 = {
|
||||
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<string[]>([]);
|
||||
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({
|
|||
</select>
|
||||
</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">
|
||||
<label className="text-sm font-medium text-strong">
|
||||
Agent prompt{" "}
|
||||
|
|
|
|||
Loading…
Reference in New Issue