mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix(local-model): claim the connection first, keep an outcome the disk refused, say why a start failed
The connect step records the connection in the state file BEFORE it writes the endpoint record and the settings flag (the flag's earlier value read by inspectLocalEndpoint()), so a state write that fails or a server killed between the two never leaves a record of ours that the state file does not know; a record under the reserved id that says exactly what ours would say is ours. changePort() writes the state first and puts the old port back when the endpoint move is refused. A job whose end could not be written (a full disk) is kept in memory: the view shows it, Resume and a new install start from it, the next verb writes it, and Cancel never answers a 500. A first install on a full disk is a 507 with the sentence. "installing" in the file while this process runs no job always reads as paused or that failure. An installed model whose runtime does not start says so (runtime-start-failed with Start as the next step, and the launch check says it too); the stored runtime version is started while its folder is unpacked. Stop, Remove and the boot of anything that is not a running installation end a runtime an earlier server left behind before any file is deleted. Stop and "Use another port" answer 409 sessions-open while a live session uses the model. A pull that failed because the runtime is gone is runtime-stopped, not a network problem. After "does not fit" only a model that needs less memory is offered. A replaced job's partial blobs are deleted. Every 400 carries its sentence as data.message, and the six inline sentences are labels with zh-CN entries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
6319fffce1
commit
fa978008d2
13 files changed
+1647
-184
No files matched your search
@@ -30,7 +30,7 @@
|
||||
*/
|
||||
|
||||
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
||||
import { ApiErrorCode, createErrorResponse, type ApiResponse } from '../../types.js';
|
||||
import { ApiErrorCode, createErrorResponse, LOCAL_MODEL_INSTALL_LABELS, type ApiResponse } from '../../types.js';
|
||||
import { isAdmin, parseBody, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
|
||||
import { isMultiUserMode } from '../../config/multiuser.js';
|
||||
import { getDataDir } from '../../config/instance.js';
|
||||
@@ -933,10 +933,7 @@ function refusedHostsWrite(err: unknown, reply: { code: (n: number) => unknown }
|
||||
|
||||
/** The answer to an edit or a delete of the record the local model install manages. */
|
||||
function managedEndpointRefusal(): ApiResponse<never> {
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.INVALID_INPUT,
|
||||
'This endpoint belongs to the model Codeman runs on this Mac. Change or remove it in the local model window.'
|
||||
);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, LOCAL_MODEL_INSTALL_LABELS.endpointManaged);
|
||||
}
|
||||
|
||||
export function registerCustomModelRoutes(app: FastifyInstance): void {
|
||||
@@ -956,10 +953,7 @@ export function registerCustomModelRoutes(app: FastifyInstance): void {
|
||||
const badDefault = invalidDefaultModel(host);
|
||||
if (badDefault) return badDefault;
|
||||
if (isLocalModelEndpointId(host.id)) {
|
||||
return createErrorResponse(
|
||||
ApiErrorCode.INVALID_INPUT,
|
||||
`The endpoint id "${host.id}" is reserved for the model Codeman runs on this Mac. Pick another id.`
|
||||
);
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, LOCAL_MODEL_INSTALL_LABELS.endpointIdReserved);
|
||||
}
|
||||
try {
|
||||
return await mutateCustomModelHosts<ApiResponse<{ host: RedactedHost }>>((hosts) => {
|
||||
|
||||
@@ -40,11 +40,14 @@
|
||||
* - A verb answers the view AFTER the verb. A refusal the service names is a 400 (the request
|
||||
* is not a row the server offers) or a 409 whose `data` is the `LocalModelConflict`
|
||||
* (`reason`, the sentence, and for `size-changed` the server's own total, for
|
||||
* `sessions-open` the session names). A write of the settings or of the endpoint store
|
||||
* that was refused (a file that does not parse) is a 409 without `data`, with the fix.
|
||||
* `sessions-open` the session names). A 400 the service names carries its sentence as
|
||||
* `data.message` too, so the window prints it in its own language. A write of the settings
|
||||
* or of the endpoint store that was refused (a file that does not parse) is a 409 without
|
||||
* `data`, with the fix.
|
||||
* - The verbs do the work in the install service; this file only gates, parses and maps.
|
||||
* `DELETE` is the one that looks at the live sessions: it hands their names to the service,
|
||||
* which refuses while one of them still uses the model.
|
||||
* `DELETE`, `runtime` and `resume` look at the live sessions: they hand their names to the
|
||||
* service, which refuses a removal, a stop and a change of port while one of them still
|
||||
* uses the model.
|
||||
* - Every dependency is injected (`LocalModelRouteDeps`), so a route test spawns nothing and
|
||||
* does not need a Mac: it passes its own hardware, harness facts and service.
|
||||
*/
|
||||
@@ -61,6 +64,7 @@ import {
|
||||
type LocalModelCatalog,
|
||||
type LocalModelConflict,
|
||||
type LocalModelInstallView,
|
||||
type LocalModelRefusal,
|
||||
type LocalModelStatus,
|
||||
type Recommendation,
|
||||
type RecommendOptions,
|
||||
@@ -152,8 +156,20 @@ export interface LocalModelConflictResponse {
|
||||
data: LocalModelConflict;
|
||||
}
|
||||
|
||||
/** What a verb answers: the view after it, an error envelope, or the 409 that carries the conflict. */
|
||||
export type LocalModelVerbResponse = ApiResponse<LocalModelInstallView> | LocalModelConflictResponse;
|
||||
/** The 400 (or 507) of a verb the service refused with a sentence: the error envelope plus that sentence as `data`. */
|
||||
export interface LocalModelRefusalResponse {
|
||||
success: false;
|
||||
/** The sentence in English, its slots filled. */
|
||||
error: string;
|
||||
errorCode: ApiErrorCode;
|
||||
data: LocalModelRefusal;
|
||||
}
|
||||
|
||||
/** What a verb answers: the view after it, an error envelope, or a refusal that carries its sentence. */
|
||||
export type LocalModelVerbResponse =
|
||||
| ApiResponse<LocalModelInstallView>
|
||||
| LocalModelConflictResponse
|
||||
| LocalModelRefusalResponse;
|
||||
|
||||
/** 403 for a non-admin in multi-user mode, before anything else runs. `null`: go on. */
|
||||
function adminOnly(req: FastifyRequest, reply: FastifyReply): ApiResponse<never> | null {
|
||||
@@ -170,7 +186,14 @@ function actorOf(req: FastifyRequest): LocalModelActor {
|
||||
function respond(result: LocalModelVerbResult, reply: FastifyReply): LocalModelVerbResponse {
|
||||
if (result.ok) return { success: true, data: result.view };
|
||||
reply.code(result.status);
|
||||
if (result.status === 400) return createErrorResponse(ApiErrorCode.INVALID_INPUT, result.message);
|
||||
if (result.status !== 409) {
|
||||
return {
|
||||
success: false,
|
||||
error: formatInstallMessage(result.message),
|
||||
errorCode: result.status === 400 ? ApiErrorCode.INVALID_INPUT : ApiErrorCode.OPERATION_FAILED,
|
||||
data: { message: result.message },
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: formatInstallMessage(result.conflict.message),
|
||||
@@ -258,14 +281,18 @@ export function registerLocalModelRoutes(
|
||||
const denied = adminOnly(req, reply);
|
||||
if (denied) return denied;
|
||||
const body = parseBody(LocalModelResumeSchema, req.body ?? {});
|
||||
return runVerb(reply, () => resolved.service().resume(body, actorOf(req)));
|
||||
const sessionNames = sessionNamesUsingEndpoint(ctx.sessions.values());
|
||||
return runVerb(reply, () => resolved.service().resume(body, actorOf(req), sessionNames));
|
||||
});
|
||||
|
||||
app.post('/api/local-model/runtime', async (req, reply): Promise<LocalModelVerbResponse> => {
|
||||
const denied = adminOnly(req, reply);
|
||||
if (denied) return denied;
|
||||
const body = parseBody(LocalModelRuntimeSchema, req.body);
|
||||
return runVerb(reply, () => resolved.service().runtime(body));
|
||||
// Asked at the moment of the request, like the DELETE: a stop or a change of port leaves
|
||||
// a live session calling an address nothing of ours holds.
|
||||
const sessionNames = sessionNamesUsingEndpoint(ctx.sessions.values());
|
||||
return runVerb(reply, () => resolved.service().runtime(body, sessionNames));
|
||||
});
|
||||
|
||||
app.delete('/api/local-model', async (req, reply): Promise<LocalModelVerbResponse> => {
|
||||
|
||||
Reference in new issue
Block a user