From 179ac13428f8ad6e635c707aa19e39dab57902e9 Mon Sep 17 00:00:00 2001 From: JuheeShin Date: Mon, 22 Sep 2025 10:07:59 +0000 Subject: [PATCH] Enhance R1 Interface API Specifications for Standard Compliance MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Update the R1 interface API specifications and implementation to comply with O-RAN standards and RFC 7807 guidelines. Several deviations from O-RAN R1 specifications and API best practices were identified and fixed: - Schema Consistency: Added missing fields (artifactVersion, owner) and ensured inputDataType/outputDataType are arrays with targetEnvironment included. - Error Handling: Standardized error responses (400/404/500) to use RFC 7807 ProblemDetails format. - Naming Consistency: Unified path parameters and schema naming to follow camelCase (e.g., id → modelId). The artifactVersion and targetEnvironment fields are planned to be incorporated in a follow-up patch. Issue-ID: AIMLFW-256 Change-Id: I01365edf62b25eab4b704c7c9c455802122b2ce2 Signed-off-by: JuheeShin --- API_docs/mme.yaml | 225 +++++++++++++++++++++++++++++++++++++++++++----------- apis/mmes_apis.go | 42 +++++----- 2 files changed, 200 insertions(+), 67 deletions(-) diff --git a/API_docs/mme.yaml b/API_docs/mme.yaml index 7b6eb24..971fd1d 100644 --- a/API_docs/mme.yaml +++ b/API_docs/mme.yaml @@ -1,8 +1,8 @@ openapi: 3.0.0 info: title: Model Management API - description: API for managing machine learning models (register, retrieve, update, delete, upload, and download). - version: 1.1.0 + description: API for managing machine learning models (register, retrieve, update, delete, upload, and download) + version: 1.0.0 servers: - url: http://11.0.0.4:32006 @@ -26,16 +26,37 @@ paths: schema: $ref: '#/components/schemas/ModelRelatedInformation' responses: - '200': + '201': description: Model registered successfully and returned object + headers: + Location: + description: 'Contains the URI of the newly created resource' + required: true + schema: + type: string + example: "/ai-ml-model-registration/v1/model-registrations/123e4567-e89b-12d3-a456-426614174000" content: application/json: schema: $ref: '#/components/schemas/ModelRelatedInformation' '400': description: Invalid request, bad input data + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '409': + description: Conflict – model name and version combination already exists + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' /ai-ml-model-discovery/v1/models: get: @@ -47,15 +68,6 @@ paths: - name: modelName in: query description: Model name to search - required: false - schema: - type: string - - name: modelVersion - in: query - description: Model version to search - required: false - schema: - type: string responses: '200': description: A list of models or filtered search results @@ -67,20 +79,30 @@ paths: $ref: '#/components/schemas/ModelRelatedInformation' '400': description: Invalid query parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + /ai-ml-model-registration/v1/model-registrations/{modelRegistrationId}: get: tags: - Model Management - summary: Get model info by ID + summary: Get model info by modelRegistrationId operationId: getModelInfoById parameters: - - name: id + - name: modelRegistrationId in: path required: true schema: type: string + example: "123e4567-e89b-12d3-a456-426614174000" responses: '200': description: Model information @@ -90,20 +112,29 @@ paths: $ref: '#/components/schemas/ModelRelatedInformation' '404': description: Model not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' put: tags: - Model Management - summary: Update model info by ID + summary: Update model info by modelRegistrationId operationId: updateModel parameters: - - name: id + - name: modelRegistrationId in: path required: true schema: type: string + example: "123e4567-e89b-12d3-a456-426614174000" requestBody: required: true content: @@ -113,29 +144,58 @@ paths: responses: '200': description: Model updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ModelRelatedInformation' '400': description: Invalid request body + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '404': + description: Model not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' delete: tags: - Model Management - summary: Delete a model by ID + summary: Delete a model by modelRegistrationId operationId: deleteModel parameters: - - name: id + - name: modelRegistrationId in: path required: true schema: type: string + example: "123e4567-e89b-12d3-a456-426614174000" responses: - '200': + '204': description: Model deleted successfully + '404': + description: Model not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' - /ai-ml-model-registration/v1/uploadModel/{modelName}/{modelVersion}/{artifactVersion}: + /ai-ml-model-registration/v1/uploadModel/{modelName}/{modelVersion}: post: tags: - Model Management @@ -152,11 +212,6 @@ paths: required: true schema: type: string - - name: artifactVersion - in: path - required: true - schema: - type: string requestBody: required: true content: @@ -167,8 +222,10 @@ paths: file: type: string format: binary + required: + - file responses: - '200': + '201': description: Model uploaded successfully content: application/json: @@ -177,14 +234,24 @@ paths: properties: code: type: integer - example: 200 + example: 201 message: type: string example: "Model uploaded successfully." + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' - /ai-ml-model-registration/v1/downloadModel/{modelName}/{modelVersion}/{artifactVersion}/model.zip: + /ai-ml-model-registration/v1/downloadModel/{modelName}/{modelVersion}/model.zip: get: tags: - Model Management @@ -201,11 +268,6 @@ paths: required: true schema: type: string - - name: artifactVersion - in: path - required: true - schema: - type: string responses: '200': description: Model downloaded successfully @@ -214,27 +276,82 @@ paths: schema: type: string format: binary + '404': + description: Model not found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' components: schemas: + ProblemDetails: + type: object + properties: + type: + type: string + format: uri + description: A URI reference that identifies the problem type + example: "https://o-ran.org/problems/invalid-model-format" + default: "about:blank" + title: + type: string + description: A short, human-readable summary of the problem + example: "Invalid Model Format" + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem + example: "The provided model file format is not supported. Expected formats: .pkl, .joblib, .h5" + status: + type: integer + description: The HTTP status code + example: 400 + minimum: 100 + maximum: 599 + instance: + type: string + format: uri + description: A URI reference that identifies the specific occurrence of the problem + example: "/ai-ml-model-registration/v1/model-registrations/invalid-model-123" + required: + - type + - title + Metadata: type: object properties: author: type: string + description: "Author of the AI/ML model" example: "John Doe" + owner: + type: string + description: "Owner information for model usage regulation" + example: "AI Research Team" + required: + - author + - owner - ModelID: + ModelId: type: object properties: modelName: type: string + description: "Name of the model" example: "example-model" modelVersion: type: string + description: "Version of the model" example: "v1.0" + required: + - modelName + - modelVersion ModelInformation: type: object @@ -242,24 +359,46 @@ components: metadata: $ref: '#/components/schemas/Metadata' inputDataType: - type: string - description: 'Input data type for the model' + type: array + items: + type: string + description: 'Supported input data types for the model' + example: ["sensor_data", "network_metrics"] + minItems: 1 outputDataType: - type: string - description: 'Output data type for the model' - + type: array + items: + type: string + description: 'Expected output data types from the model' + example: ["prediction_result", "anomaly_score"] + minItems: 1 + required: + - metadata + - inputDataType + - outputDataType ModelRelatedInformation: type: object properties: - id: + modelRegistrationId: type: string + description: "Unique identifier for the model registration" example: "123e4567-e89b-12d3-a456-426614174000" modelId: - $ref: '#/components/schemas/ModelID' + $ref: '#/components/schemas/ModelId' description: type: string - example: "This is an example model description." + description: "Description of the AI/ML model" + example: "This is an example model for network anomaly detection." modelInformation: $ref: '#/components/schemas/ModelInformation' - + modelLocation: + type: string + format: uri + description: "Location where the model is stored in the runtime catalogue" + example: "https://model-registry.example.com/models/example-model/v1.0" + required: + - modelRegistrationId + - modelId + - description + - modelInformation \ No newline at end of file diff --git a/apis/mmes_apis.go b/apis/mmes_apis.go index 96f9c0d..af9bd21 100644 --- a/apis/mmes_apis.go +++ b/apis/mmes_apis.go @@ -80,6 +80,7 @@ func (m *MmeApiHandler) RegisterModel(cont *gin.Context) { }) return } + // by default when a model is registered its artifact version is set to 0.0.0 modelInfo.ModelId.ArtifactVersion = "0.0.0" @@ -90,18 +91,17 @@ func (m *MmeApiHandler) RegisterModel(cont *gin.Context) { if pqErr.Code == pgerrcode.UniqueViolation { cont.JSON(http.StatusConflict, models.ProblemDetail{ Status: http.StatusConflict, - Title: "model name and version combination already present", - Detail: "The request json is not correct as", - }) - return - } else { - cont.JSON(http.StatusInternalServerError, models.ProblemDetail{ - Status: http.StatusInternalServerError, - Title: "Bad Request", - Detail: "The request json is not correct as", + Title: "Conflict", + Detail: "model name and version combination already present", }) return } + cont.JSON(http.StatusInternalServerError, models.ProblemDetail{ + Status: http.StatusInternalServerError, + Title: "Internal Server Error", + Detail: fmt.Sprintf("Database error: %s", err.Error()), + }) + return } } @@ -110,17 +110,15 @@ func (m *MmeApiHandler) RegisterModel(cont *gin.Context) { cont.JSON(http.StatusCreated, gin.H{ "modelInfo": modelInfo, }) - } /* This API retrieves model info list managed in modelmgmtservice */ func (m *MmeApiHandler) GetModelInfo(cont *gin.Context) { - logging.INFO("Get model info ") queryParams := cont.Request.URL.Query() - //to check only modelName and modelVersion can be passed. + // to check only modelName and modelVersion can be passed. allowedParams := map[string]bool{ MODELNAME: true, MODELVERSION: true, @@ -142,8 +140,7 @@ func (m *MmeApiHandler) GetModelInfo(cont *gin.Context) { modelVersion := cont.Query(MODELVERSION) if modelName == "" && modelVersion == "" { - //return all modelinfo stored - + // return all modelinfo stored models, err := m.iDB.GetAll() if err != nil { logging.ERROR("error:", err) @@ -169,10 +166,8 @@ func (m *MmeApiHandler) GetModelInfo(cont *gin.Context) { }) return } - cont.JSON(http.StatusOK, modelInfos) return - } else { // get all modelInfo by model name and version modelInfo, err := m.iDB.GetModelInfoByNameAndVer(modelName, modelVersion) @@ -190,13 +185,12 @@ func (m *MmeApiHandler) GetModelInfo(cont *gin.Context) { statusCode := http.StatusNotFound logging.ERROR("Record not found, send status code: ", statusCode) cont.JSON(statusCode, models.ProblemDetail{ - Status: http.StatusInternalServerError, - Title: "Internal Server Error", + Status: http.StatusNotFound, + Title: "Not Found", Detail: fmt.Sprintf("Record not found with modelName: %s and modelVersion: %s", modelName, modelVersion), }) return } - response := []models.ModelRelatedInformation{*modelInfo} cont.JSON(http.StatusOK, response) return @@ -265,7 +259,7 @@ func (m *MmeApiHandler) UploadModel(cont *gin.Context) { modelKey := fmt.Sprintf("%s_%s_%s", modelName, modelVersion, artifactVersion) exportBucket := strings.ToLower(modelName) - //TODO convert multipart.FileHeader to []byted + //TODO convert multipart.FileHeader to []byte fileHeader, _ := cont.FormFile("file") //TODO : Accept only .zip file for trained model file, _ := fileHeader.Open() @@ -309,7 +303,7 @@ func (m *MmeApiHandler) DownloadModel(cont *gin.Context) { }) return } - //Return file in api reponse using byte slice + // Return file in api response using byte slice cont.Header("Content-Disposition", "attachment;"+fileName) cont.Header("Content-Type", "application/zip") cont.Data(http.StatusOK, "application/octet", fileByes) @@ -342,12 +336,13 @@ func (m *MmeApiHandler) UpdateModel(c *gin.Context) { }) return } + if existingModelInfo.ModelId.ModelName != modelInfo.ModelId.ModelName || existingModelInfo.ModelId.ModelVersion != modelInfo.ModelId.ModelVersion { statusCode := http.StatusBadRequest logging.ERROR("Error occurred, send status code: ", statusCode) c.JSON(statusCode, gin.H{ "code": statusCode, - "message": fmt.Sprintf("model with id: %s have different modelname and modelversion than provided", id), + "message": fmt.Sprintf("model with id: %s has different modelName and modelVersion than provided", id), }) return } @@ -380,7 +375,6 @@ func (m *MmeApiHandler) DeleteModel(cont *gin.Context) { func (m *MmeApiHandler) UpdateArtifact(cont *gin.Context) { logging.INFO("Update artifact version of model") - // var modelInfo *models.ModelRelatedInformation modelname := cont.Param("modelname") modelversion := cont.Param("modelversion") artifactversion := cont.Param("artifactversion") @@ -395,7 +389,7 @@ func (m *MmeApiHandler) UpdateArtifact(cont *gin.Context) { } modelInfo.ModelId.ArtifactVersion = artifactversion if err := m.iDB.Update(*modelInfo); err != nil { - logging.ERROR("error in updated db", "error:", err) + logging.ERROR("error in update db", "error:", err) return } logging.INFO("model updated") -- 2.16.6