feat(ai-router): test an endpoint before saving it, and re-point a live lane
Two gaps left by the AI Router feature: a profile could be saved without ever
being reachable, and a lane's router could only be chosen at creation.
**POST /api/ai-routers/test** answers the question saving cannot. It sends one
Anthropic-style message capped at a single output token to `<baseUrl>/v1/messages`
with a 15s timeout — the wire shape a claude lane's eight env keys point at, so
what is tested is what will actually run. A router that answers is a 200 here
whatever it replies: `{ ok, status, message }` carries the router's own words,
and `status` is null when nothing answered at all. Only bad input is a 4xx.
The stored key is only ever sent to the endpoint it was stored against. A blank
`apiKey` falls back to the key of the profile named by `name`, but only while
the submitted `baseUrl` still equals the saved one — so testing a retyped
endpoint needs a retyped key (`EBADKEY`). Without that rule this route would be
a way to make the server POST a saved API key to any URL a browser names.
Network failures report `error.cause`, not `error.message`: node's fetch reports
every transport failure as the same useless "fetch failed" and hides the part
the user can act on (ECONNREFUSED, ENOTFOUND, a TLS error) one level down.
**The lane card carries an AI Router select.** The Add-lane modal was the only
place a router was chosen, and a lane outlives its creation. Switching goes
through `POST /api/lanes/:id/router/apply` — never a lane PATCH — because the
DB column and the lane's own config on disk may only move together. A failed
apply puts the select back rather than leaving the card claiming a router the
lane did not get. Hidden when `native` is the only profile: a select with one
option is a control that cannot do anything.
Adds `Lane.router_profile` to the client type (the payload already carried it).
This commit is contained in:
@@ -27,6 +27,7 @@ const { sameOriginGuard } = require("../lib/same-origin");
|
||||
const router = express.Router();
|
||||
|
||||
const ERR_TO_STATUS = {
|
||||
EBADKEY: 400,
|
||||
EBADNAME: 400,
|
||||
EBADLABEL: 400,
|
||||
EBADURL: 400,
|
||||
@@ -59,6 +60,22 @@ router.post("/", sameOriginGuard, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/ai-routers/test — does this endpoint + key + model actually answer?
|
||||
// Body: `{ baseUrl, model, apiKey?, name? }`. Registered before `/:name` for
|
||||
// clarity; no collision today, since `/:name` takes no POST.
|
||||
//
|
||||
// A reachable router is a 200 here whatever it replies — `{ ok:false, status }`
|
||||
// carries the router's own words, which is the whole point. Only bad input is
|
||||
// a 4xx. The response never echoes the key, including on the path where the
|
||||
// stored one was used (see `testRouter` on why that path is narrow).
|
||||
router.post("/test", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
res.json(await routers.testRouter(req.body || {}));
|
||||
} catch (e) {
|
||||
return fail(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/ai-routers/:name — same fields, all optional. An omitted `apiKey`
|
||||
// keeps the stored one; the edit form cannot show a key, so it cannot echo one.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user