feat: complete TASK-WP3-01 model configuration

This commit is contained in:
suyx
2026-08-03 02:40:48 +08:00
parent 1b0a05bbcf
commit d79ac74fdd
19 changed files with 7018 additions and 3 deletions
+142
View File
@@ -49,6 +49,17 @@ import {
LogoutResponseSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
ModelIdSchema,
ModelContractValidationStatusSchema,
ModelRuntimeReasonSchema,
ModelReferenceLimitsSchema,
ModelRuntimeAvailabilitySchema,
ModelConfigurationResponseSchema,
ModelConfigSchema,
ModelConfigCandidateSchema,
ModelConfigUpdateRequestSchema,
ModelParamsSchema,
ModelConfigUpdateHeadersSchema,
FailedEmptyTrashRequestSchema,
FailedEmptyTrashResponseSchema,
ExportFormatSchema,
@@ -151,6 +162,8 @@ import {
registrationFieldError,
} from "./registration-errors.js";
import type { RegistrationService } from "./registration.js";
import { ModelConfigurationError } from "./model-configuration.js";
import type { ModelConfigurationService } from "./model-configuration.js";
const defaultBootstrap: BootstrapResponse = {
app_version: "0.0.0",
@@ -173,6 +186,7 @@ export interface CreateAppOptions {
eventHub?: EventHub;
generations?: GenerationSubmissionService;
latestExports?: LatestExportService;
models?: ModelConfigurationService;
networkBoundary?: NetworkBoundaryOptions;
publicAssets?: PublicAssetResolver;
projects?: ProjectService;
@@ -316,6 +330,22 @@ function creditFailure(reply: FastifyReply, correlationId: string, error: unknow
return reply.code(status).send(null);
}
function modelConfigurationFailure(reply: FastifyReply, correlationId: string, error: unknown) {
if (!(error instanceof ModelConfigurationError)) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId }));
}
const status = error.code === "MODEL_CONFIG_VERSION_CONFLICT" ? 412
: error.code === "MODEL_RECOMMENDATION_PRIORITY_INVALID" ? 400
: error.code === "IDEMPOTENCY_KEY_CONFLICT" ? 409 : 409;
const details = {
...(error.code === "MODEL_CONFIG_VERSION_CONFLICT" && typeof error.details.latest_version === "number"
? { latest_version: error.details.latest_version } : {}),
...(Array.isArray(error.details.conflict_model_ids) ? { conflict_model_ids: error.details.conflict_model_ids as string[] } : {}),
...(Array.isArray(error.details.field_errors) ? { field_errors: error.details.field_errors as Array<{ field: string; message_key: string }> } : {}),
};
return reply.code(status).send(createErrorEnvelope({ code: error.code, correlationId, details }));
}
function generationTaskResponse(task: GenerationTaskView) {
return {
confirmed_credit_cost: task.confirmedCreditCost,
@@ -698,6 +728,17 @@ export async function createApp(options: CreateAppOptions = {}) {
ProjectStateConflictResponseSchema,
FailedEmptyTrashRequestSchema,
FailedEmptyTrashResponseSchema,
ModelIdSchema,
ModelContractValidationStatusSchema,
ModelRuntimeReasonSchema,
ModelReferenceLimitsSchema,
ModelRuntimeAvailabilitySchema,
ModelConfigSchema,
ModelConfigCandidateSchema,
ModelConfigurationResponseSchema,
ModelParamsSchema,
ModelConfigUpdateRequestSchema,
ModelConfigUpdateHeadersSchema,
]) {
app.addSchema(schema);
}
@@ -2163,6 +2204,107 @@ export async function createApp(options: CreateAppOptions = {}) {
},
);
app.get(
"/api/v1/models",
{
schema: {
operationId: "getModels",
response: { 200: ModelConfigurationResponseSchema, 503: ErrorEnvelopeSchema, 426: ErrorEnvelopeSchema },
tags: ["Models"],
},
},
async (request, reply) => {
if (!options.models) return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
try {
return options.models.read();
} catch {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
},
);
app.get(
"/api/v1/models/:model_id",
{
schema: {
operationId: "getModel",
params: ModelParamsSchema,
response: { 200: ModelConfigSchema, 404: Type.Null(), 503: ErrorEnvelopeSchema, 426: ErrorEnvelopeSchema },
tags: ["Models"],
},
},
async (request, reply) => {
if (!options.models) return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
const model = options.models.readModel((request.params as { model_id: string }).model_id);
return model ?? reply.code(404).send(null);
},
);
app.put(
"/api/v1/admin/models/configuration",
{
attachValidation: true,
schema: {
operationId: "replaceModelConfiguration",
body: ModelConfigUpdateRequestSchema,
headers: ModelConfigUpdateHeadersSchema,
response: {
200: ModelConfigurationResponseSchema,
400: ErrorEnvelopeSchema,
401: ErrorEnvelopeSchema,
403: ErrorEnvelopeSchema,
409: ErrorEnvelopeSchema,
412: ErrorEnvelopeSchema,
429: ErrorEnvelopeSchema,
503: ErrorEnvelopeSchema,
426: ErrorEnvelopeSchema,
},
tags: ["Admin Models"],
},
},
async (request, reply) => {
if (request.validationError) {
const validation = JSON.stringify(request.validationError.validation ?? []);
const priority = validation.includes("recommendation_priority");
return reply.code(priority ? 400 : 409).send(createErrorEnvelope({
code: priority ? "MODEL_RECOMMENDATION_PRIORITY_INVALID" : "MODEL_DEFAULT_REPLACEMENT_INVALID",
correlationId: request.id,
...(priority ? { details: { field_errors: [{ field: "models.recommendation_priority", message_key: "model.priority.invalid" }] } } : {}),
}));
}
if (!options.models || !options.registration) {
return reply.code(503).send(createErrorEnvelope({ code: "AUTH_SERVICE_UNAVAILABLE", correlationId: request.id }));
}
const token = cookieValue(headerValue(request.headers.cookie), adminSessionCookieName);
const session = token ? options.registration.readAdminSession(token) : undefined;
if (!token || !session) return reply.code(401).send(createErrorEnvelope({ code: "AUTH_SESSION_INVALID", correlationId: request.id }));
try {
const headers = request.headers as { "idempotency-key": string; "x-csrf-token": string };
const admin = options.registration.authorizeAdminMutation({ csrfToken: headers["x-csrf-token"], sessionToken: token });
const body = request.body as {
expected_config_set_version: number;
models: Array<Record<string, unknown>>;
};
const result = options.models.replace({
actorId: admin.userId,
expectedConfigSetVersion: body.expected_config_set_version,
idempotencyKey: headers["idempotency-key"],
models: body.models as never,
});
return result;
} catch (error) {
if (error instanceof RegistrationError) {
return reply.code(error.httpStatus).send(createErrorEnvelope({
code: error.code,
correlationId: request.id,
details: { field_errors: [registrationFieldError(error.reason)] },
}));
}
return modelConfigurationFailure(reply, request.id, error);
}
},
);
app.get(
"/api/v1/bootstrap",
{