diff --git a/backend/cmd/gorm-gen/curd/generate.go b/backend/cmd/gorm-gen/curd/generate.go index f06318daa..fb8c8c5b7 100644 --- a/backend/cmd/gorm-gen/curd/generate.go +++ b/backend/cmd/gorm-gen/curd/generate.go @@ -54,6 +54,9 @@ func main() { model.PrequeueConfig{}, model.QueueQuotaLimit{}, model.UserBanRecord{}, + model.KthenaChatSession{}, + model.KthenaChatMessage{}, + model.KthenaInferenceTemplate{}, ) // 执行并生成代码 diff --git a/backend/cmd/gorm-gen/models/migrate.go b/backend/cmd/gorm-gen/models/migrate.go index f14140496..9d2650dd8 100644 --- a/backend/cmd/gorm-gen/models/migrate.go +++ b/backend/cmd/gorm-gen/models/migrate.go @@ -142,6 +142,36 @@ func modelDownloadSubmissionMigration() *gormigrate.Migration { } } +func kthenaChatMigration() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "202608021430", + Migrate: func(tx *gorm.DB) error { + if err := createTableIfMissing(tx, &model.KthenaChatSession{}); err != nil { + return err + } + return createTableIfMissing(tx, &model.KthenaChatMessage{}) + }, + Rollback: func(tx *gorm.DB) error { + if err := dropTableIfPresent(tx, &model.KthenaChatMessage{}); err != nil { + return err + } + return dropTableIfPresent(tx, &model.KthenaChatSession{}) + }, + } +} + +func kthenaInferenceTemplateMigration() *gormigrate.Migration { + return &gormigrate.Migration{ + ID: "202608021500", + Migrate: func(tx *gorm.DB) error { + return createTableIfMissing(tx, &model.KthenaInferenceTemplate{}) + }, + Rollback: func(tx *gorm.DB) error { + return dropTableIfPresent(tx, &model.KthenaInferenceTemplate{}) + }, + } +} + func createTableIfMissing(db *gorm.DB, value any) error { if db.Migrator().HasTable(value) { return nil @@ -1572,6 +1602,8 @@ func main() { return dropColumnIfPresent(tx, "users", &User{}, "BannedTimestamp") }, }, + kthenaChatMigration(), + kthenaInferenceTemplateMigration(), modelDownloadSubmissionMigration(), }) @@ -1609,6 +1641,9 @@ func main() { &model.PrequeueConfig{}, &model.QueueQuotaLimit{}, &model.UserBanRecord{}, + &model.KthenaChatSession{}, + &model.KthenaChatMessage{}, + &model.KthenaInferenceTemplate{}, ) if err != nil { return err diff --git a/backend/cmd/gorm-gen/models/migrate_test.go b/backend/cmd/gorm-gen/models/migrate_test.go index 70a2eef3d..f62c7914d 100644 --- a/backend/cmd/gorm-gen/models/migrate_test.go +++ b/backend/cmd/gorm-gen/models/migrate_test.go @@ -126,3 +126,59 @@ func TestModelDownloadSubmissionMigrationAndRollback(t *testing.T) { t.Fatal("model download submission table remains after rollback") } } + +func TestKthenaChatMigrationAndRollback(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_chat_migration?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + migration := kthenaChatMigration() + if err := migration.Migrate(db); err != nil { + t.Fatalf("migrate: %v", err) + } + if err := migration.Migrate(db); err != nil { + t.Fatalf("idempotent migrate: %v", err) + } + for _, table := range []any{&model.KthenaChatSession{}, &model.KthenaChatMessage{}} { + if !db.Migrator().HasTable(table) { + t.Fatalf("missing migrated table %T", table) + } + } + if err := migration.Rollback(db); err != nil { + t.Fatalf("rollback: %v", err) + } + if err := migration.Rollback(db); err != nil { + t.Fatalf("idempotent rollback: %v", err) + } + for _, table := range []any{&model.KthenaChatSession{}, &model.KthenaChatMessage{}} { + if db.Migrator().HasTable(table) { + t.Fatalf("table remains after rollback: %T", table) + } + } +} + +func TestKthenaInferenceTemplateMigrationAndRollback(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_inference_template_migration?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + migration := kthenaInferenceTemplateMigration() + if err := migration.Migrate(db); err != nil { + t.Fatalf("migrate: %v", err) + } + if err := migration.Migrate(db); err != nil { + t.Fatalf("idempotent migrate: %v", err) + } + if !db.Migrator().HasTable(&model.KthenaInferenceTemplate{}) { + t.Fatal("Kthena inference template table is missing") + } + if err := migration.Rollback(db); err != nil { + t.Fatalf("rollback: %v", err) + } + if err := migration.Rollback(db); err != nil { + t.Fatalf("idempotent rollback: %v", err) + } + if db.Migrator().HasTable(&model.KthenaInferenceTemplate{}) { + t.Fatal("Kthena inference template table remains after rollback") + } +} diff --git a/backend/dao/model/kthena_chat.go b/backend/dao/model/kthena_chat.go new file mode 100644 index 000000000..900a84798 --- /dev/null +++ b/backend/dao/model/kthena_chat.go @@ -0,0 +1,63 @@ +package model + +import ( + "time" + + "gorm.io/datatypes" +) + +// KthenaChatSession stores one user's conversation with one routed Kthena +// deployment. ClientSessionID is deliberately an application-level UUID: the +// web client can keep using its existing local UUID while the database keeps a +// compact numeric primary key for message joins. +// +// A session is scoped by user, account, namespace, deployment and served +// model. The scope is derived from the authorized ModelBooster object rather +// than from a client supplied model name. +// +//nolint:lll // Composite GORM index declarations must stay in a single struct tag. +type KthenaChatSession struct { + ID uint `gorm:"primarykey"` + + UserID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_session_scope,priority:1;index:idx_kthena_chat_session_list,priority:1;comment:会话所属用户ID"` + AccountID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_session_scope,priority:2;index:idx_kthena_chat_session_list,priority:2;comment:会话所属账户ID"` + Username string `gorm:"type:varchar(64);not null;comment:创建会话时的用户名快照"` + + Namespace string `gorm:"type:varchar(63);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:3;index:idx_kthena_chat_session_list,priority:3;comment:模型部署命名空间"` + ServiceName string `gorm:"type:varchar(63);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:4;index:idx_kthena_chat_session_list,priority:4;comment:ModelBooster名称"` + ModelName string `gorm:"type:varchar(256);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:5;index:idx_kthena_chat_session_list,priority:5;comment:路由模型名快照"` + BackendType string `gorm:"type:varchar(64);not null;comment:推理后端快照"` + + ClientSessionID string `gorm:"type:varchar(128);not null;uniqueIndex:idx_kthena_chat_session_scope,priority:6;comment:客户端会话UUID"` + Title string `gorm:"type:varchar(256);not null;default:'';comment:会话标题"` + MessageCount int `gorm:"not null;default:0;comment:消息数"` + LastMessageAt *time.Time `gorm:"index:idx_kthena_chat_session_list,priority:6;comment:最后一条消息时间"` + CreatedAt time.Time `gorm:"not null;index:idx_kthena_chat_session_list,priority:7"` + UpdatedAt time.Time `gorm:"not null;index:idx_kthena_chat_session_list,priority:8"` +} + +func (KthenaChatSession) TableName() string { + return "kthena_chat_sessions" +} + +// KthenaChatMessage is an ordered message in a KthenaChatSession. ResponseJSON +// stores the original non-streaming OpenAI-compatible completion for safe +// idempotent retries of a client turn. +// +//nolint:lll // Composite GORM index declarations must stay in a single struct tag. +type KthenaChatMessage struct { + ID uint `gorm:"primarykey"` + SessionID uint `gorm:"not null;uniqueIndex:idx_kthena_chat_message_sequence,priority:1;uniqueIndex:idx_kthena_chat_message_turn,priority:1;index:idx_kthena_chat_message_session,priority:1;comment:会话ID"` + Sequence int `gorm:"not null;uniqueIndex:idx_kthena_chat_message_sequence,priority:2;comment:会话内消息顺序"` + Role string `gorm:"type:varchar(32);not null;comment:OpenAI消息角色"` + Content string `gorm:"type:text;not null;comment:消息正文"` + // ClientTurnID is populated only for the user message of an atomic turn. + // Nullable values keep normal CRUD message replacement unconstrained. + ClientTurnID *string `gorm:"type:varchar(128);uniqueIndex:idx_kthena_chat_message_turn,priority:2;comment:客户端幂等请求ID"` + ResponseJSON datatypes.JSON `gorm:"type:jsonb;comment:助手消息对应的原始模型响应"` + CreatedAt time.Time `gorm:"not null;index:idx_kthena_chat_message_session,priority:2"` +} + +func (KthenaChatMessage) TableName() string { + return "kthena_chat_messages" +} diff --git a/backend/dao/model/kthena_inference_template.go b/backend/dao/model/kthena_inference_template.go new file mode 100644 index 000000000..112226c97 --- /dev/null +++ b/backend/dao/model/kthena_inference_template.go @@ -0,0 +1,29 @@ +package model + +import ( + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// KthenaInferenceTemplate is a private reusable deployment preset. It is +// intentionally scoped by both user and account, so a template never becomes +// visible when a browser switches identity or account context. +// +// Config keeps the complete form payload as JSON. The deployment API remains +// authoritative when a template is eventually submitted, while templates can +// evolve without a schema migration for every new form field. +// +//nolint:lll // Composite GORM index declarations must stay in a single struct tag. +type KthenaInferenceTemplate struct { + gorm.Model + + UserID uint `gorm:"not null;uniqueIndex:idx_kthena_inference_template_scope,priority:1;index:idx_kthena_inference_template_list,priority:1;comment:模板所属用户ID"` + AccountID uint `gorm:"not null;uniqueIndex:idx_kthena_inference_template_scope,priority:2;index:idx_kthena_inference_template_list,priority:2;comment:模板所属账户ID"` + Name string `gorm:"type:varchar(64);not null;uniqueIndex:idx_kthena_inference_template_scope,priority:3;comment:模板名称"` + Description string `gorm:"type:varchar(512);not null;default:'';comment:模板说明"` + Config datatypes.JSON `gorm:"type:jsonb;not null;comment:模型部署表单配置"` +} + +func (KthenaInferenceTemplate) TableName() string { + return "kthena_inference_templates" +} diff --git a/backend/dao/model/system_config.go b/backend/dao/model/system_config.go index 3224e005c..2ee9a0da0 100644 --- a/backend/dao/model/system_config.go +++ b/backend/dao/model/system_config.go @@ -15,7 +15,8 @@ const ( ConfigKeyLLMModelName = "LLM_MODEL_NAME" // 功能开关配置键 - ConfigKeyEnableGpuAnalysis = "ENABLE_GPU_ANALYSIS" // 值: "true" or "false" + ConfigKeyEnableGpuAnalysis = "ENABLE_GPU_ANALYSIS" // 值: "true" or "false" + ConfigKeyEnableKthenaInference = "ENABLE_KTHENA_INFERENCE" // 值: "true" or "false" // Billing 功能与调度配置键 ConfigKeyEnableBillingFeature = "ENABLE_BILLING_FEATURE" @@ -48,6 +49,7 @@ var DefaultConfigKeys = []string{ ConfigKeyLLMAPIKey, ConfigKeyLLMModelName, ConfigKeyEnableGpuAnalysis, + ConfigKeyEnableKthenaInference, ConfigKeyEnableBillingFeature, ConfigKeyEnableBillingActive, ConfigKeyEnableRunningSettlement, diff --git a/backend/docs/docs.go b/backend/docs/docs.go index a233c2360..bf9743f14 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1764,6 +1764,185 @@ const docTemplate = `{ "responses": {} } }, + "/v1/admin/kthena/inference-services": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List all Kthena inference services managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "List all inference services", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/admin/kthena/inference-services/{name}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get any Kthena inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Get inference service as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete any Kthena inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Delete inference service as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/admin/kthena/inference-services/{name}/yaml": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get raw Kthena ModelBooster object for any inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Get inference service YAML as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, "/v1/admin/models/downloads": { "get": { "security": [ @@ -3297,6 +3476,74 @@ const docTemplate = `{ } } }, + "/v1/admin/system-config/kthena-inference": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。", + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "获取模型部署功能开关状态", + "responses": { + "200": { + "description": "开关状态", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "开启后,用户可以创建和管理基于 Kthena 的在线模型部署;关闭后所有模型部署接口均会拒绝访问。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "设置模型部署功能开关", + "parameters": [ + { + "description": "开关设置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.SetKthenaInferenceStatusReq" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, "/v1/admin/system-config/llm": { "get": { "security": [ @@ -6678,14 +6925,14 @@ const docTemplate = `{ } } }, - "/v1/models/download": { - "post": { + "/v1/kthena/inference-services": { + "get": { "security": [ { "Bearer": [] } ], - "description": "创建一个新的模型下载任务", + "description": "List Kthena inference services owned by the current user and account.", "consumes": [ "application/json" ], @@ -6693,38 +6940,31 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" - ], - "summary": "创建模型下载任务", - "parameters": [ - { - "description": "下载请求", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.CreateDownloadReq" - } - } + "kthena" ], + "summary": "List my inference services", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/models/downloads": { - "get": { + }, + "post": { "security": [ { "Bearer": [] } ], - "description": "下载记录对全平台用户可见。带 page 参数时返回分页结构(含状态汇总),否则返回全量数组(兼容旧客户端)", + "description": "Create a Kthena ModelBooster-backed inference service.", "consumes": [ "application/json" ], @@ -6732,59 +6972,50 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取模型下载任务列表", + "summary": "Create inference service", "parameters": [ { - "type": "string", - "description": "过滤类别: model 或 dataset", - "name": "category", - "in": "query" - }, - { - "type": "integer", - "description": "页码(从1开始);不传则返回全量数组", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "每页数量,默认20,最大100", - "name": "pageSize", - "in": "query" - }, - { - "type": "string", - "description": "过滤状态: Pending/Downloading/Paused/Ready/Failed", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "按名称模糊搜索", - "name": "search", - "in": "query" + "description": "Create inference service request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.CreateKthenaReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/models/downloads/{id}": { + "/v1/kthena/inference-services/{name}": { "get": { "security": [ { "Bearer": [] } ], - "description": "根据 ID 获取模型下载任务详情", + "description": "Get a Kthena inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -6792,14 +7023,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取单个模型下载任务详情", + "summary": "Get inference service", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true } @@ -6808,7 +7039,19 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } @@ -6819,7 +7062,7 @@ const docTemplate = `{ "Bearer": [] } ], - "description": "删除下载任务记录(仅平台管理员),已下载的文件保留在存储中", + "description": "Delete a Kthena inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -6827,14 +7070,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "删除模型下载任务", + "summary": "Delete inference service", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true } @@ -6845,55 +7088,92 @@ const docTemplate = `{ "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } } } } }, - "/v1/models/downloads/{id}/logs": { + "/v1/kthena/inference-services/{name}/conversations": { "get": { "security": [ { "Bearer": [] } ], - "description": "返回定时持久化到下载记录中的日志", - "consumes": [ - "application/json" - ], + "description": "List a user's deployment-scoped conversations; messages are omitted by default.", "produces": [ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取模型下载任务日志", + "summary": "List model deployment conversations", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true + }, + { + "type": "boolean", + "description": "Include recent messages", + "name": "includeMessages", + "in": "query" + }, + { + "type": "integer", + "description": "Conversation limit, maximum 100", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Messages per conversation, maximum 500", + "name": "messageLimit", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/models/downloads/{id}/pause": { + }, "post": { "security": [ { "Bearer": [] } ], - "description": "暂停正在进行的模型下载任务", + "description": "Create a user/deployment-scoped conversation; empty sessionId gets a UUID and supplied UUIDs are idempotent.", "consumes": [ "application/json" ], @@ -6901,36 +7181,63 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "暂停下载任务", + "summary": "Create model deployment conversation", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true + }, + { + "description": "Conversation", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationCreateReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/models/downloads/{id}/resume": { + "/v1/kthena/inference-services/{name}/conversations/turns": { "post": { "security": [ { "Bearer": [] } ], - "description": "恢复已暂停的模型下载任务", + "description": "Send an atomic turn without a path sessionId. Provide a body sessionId to reuse a UUID, or leave it empty for a new UUID.", "consumes": [ "application/json" ], @@ -6938,23 +7245,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "恢复下载任务", + "summary": "Send a new or existing persisted model deployment conversation turn", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { - "description": "可选的临时访问令牌", - "name": "data", + "description": "User turn", + "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/internal_handler.DownloadActionReq" + "$ref": "#/definitions/internal_handler.KthenaConversationTurnReq" } } ], @@ -6962,121 +7270,150 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "502": { + "description": "Bad Gateway", + "schema": {} } } } }, - "/v1/models/downloads/{id}/retry": { - "post": { + "/v1/kthena/inference-services/{name}/conversations/{sessionId}": { + "get": { "security": [ { "Bearer": [] } ], - "description": "重新提交失败的模型下载任务", - "consumes": [ - "application/json" - ], + "description": "Get one persisted conversation and its most recent ordered messages for the current user and authorized Kthena deployment.", "produces": [ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "重试失败的下载任务", + "summary": "Get model deployment conversation", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { - "description": "可选的临时访问令牌和重试版本", - "name": "data", - "in": "body", - "schema": { - "$ref": "#/definitions/internal_handler.DownloadActionReq" - } + "type": "string", + "description": "Conversation session UUID", + "name": "sessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Maximum recent messages, maximum 500", + "name": "messageLimit", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "获取Pod的容器列表", - "consumes": [ - "application/json" - ], + "description": "Permanently delete one current user's persisted Kthena conversation and all of its messages.", "produces": [ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的容器列表", + "summary": "Delete model deployment conversation", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "Pod名称", - "name": "name", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true } ], "responses": { "200": { - "description": "Pod容器列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, - "400": { - "description": "Request parameter error", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/log": { - "get": { + }, + "patch": { "security": [ { "Bearer": [] } ], - "description": "获取Pod容器日志", + "description": "Update title and/or replace all messages of a current user's conversation. Sending messages: [] clears the message list.", "consumes": [ "application/json" ], @@ -7084,96 +7421,55 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod容器日志", + "summary": "Update model deployment conversation", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", + "description": "Inference service name", "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "容器名称", - "name": "container", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true }, { - "type": "integer", - "description": "页码", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "每页数量", - "name": "size", - "in": "query", - "required": true + "description": "Conversation update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationUpdateReq" + } } ], "responses": { "200": { - "description": "Pod容器日志", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" } }, "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - } - } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/resources": { - "put": { - "description": "edit pod's resources(cpu, mem)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Operations" - ], - "summary": "edit pod's resources(cpu, mem)", - "responses": { - "200": { - "description": "Success", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "400": { - "description": "Request parameter error", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -7181,14 +7477,14 @@ const docTemplate = `{ } } }, - "/v1/namespaces/{namespace}/pods/{name}/events": { - "get": { + "/v1/kthena/inference-services/{name}/conversations/{sessionId}/turns": { + "post": { "security": [ { "Bearer": [] } ], - "description": "获取Pod的事件", + "description": "Use stored context to call Kthena and atomically save successful user and assistant messages.", "consumes": [ "application/json" ], @@ -7196,61 +7492,74 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的事件", + "summary": "Send a persisted model deployment conversation turn", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "任务名称", - "name": "name", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true + }, + { + "description": "User turn", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationTurnReq" + } } ], "responses": { "200": { - "description": "Pod事件列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp" } }, "400": { - "description": "请求参数错误", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "任务未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + }, + "502": { + "description": "Bad Gateway", + "schema": {} } } } }, - "/v1/namespaces/{namespace}/pods/{name}/ingresses": { - "get": { + "/v1/kthena/inference-services/{name}/openai/{path}": { + "post": { "security": [ { "Bearer": [] } ], - "description": "通过Pod注解获取相关的Ingress规则", + "description": "Proxy an OpenAI-compatible request to kthena-router for an inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -7258,59 +7567,65 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的Ingress规则", + "summary": "Proxy OpenAI-compatible inference request", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "Pod名称", - "name": "name", + "description": "OpenAI-compatible API path", + "name": "path", "in": "path", "required": true + }, + { + "description": "OpenAI-compatible request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.KthenaProxyReq" + } } ], "responses": { "200": { - "description": "Pod Ingress规则列表", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp" - } + "description": "OK", + "schema": {} }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "Pod未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } + "description": "Model route or runtime pod not found", + "schema": {} }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "post": { + } + }, + "/v1/kthena/inference-services/{name}/yaml": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定Pod创建新的Ingress规则,规则名称和端口号必须唯一", + "description": "Get raw Kthena ModelBooster object for an inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -7318,68 +7633,77 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "创建新的Pod Ingress规则", + "summary": "Get inference service YAML", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", + "description": "Inference service name", "name": "name", "in": "path", "required": true - }, - { - "description": "Ingress规则内容", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" - } } ], "responses": { "200": { - "description": "成功创建的Ingress规则", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误或规则冲突", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "404": { - "description": "Pod未找到", + "500": { + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + } + } + } + }, + "/v1/kthena/inference-templates": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List the current user's templates in the active account. Templates are never shared across users or accounts.", + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "List private Kthena deployment templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp" + } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "delete": { + "post": { "security": [ { "Bearer": [] } ], - "description": "根据规则名称删除指定的Ingress规则,同时删除关联的Service和Ingress", + "description": "Save the current deployment form as a private template for the current user and account.", "consumes": [ "application/json" ], @@ -7387,55 +7711,41 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "删除Pod的Ingress规则", + "summary": "Create a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "要删除的Ingress规则", - "name": "body", + "description": "Template", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateReq" } } ], "responses": { "200": { - "description": "Ingress规则删除成功", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp" } }, "400": { - "description": "请求参数错误或Ingress规则未找到", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "404": { - "description": "Pod未找到", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -7443,14 +7753,14 @@ const docTemplate = `{ } } }, - "/v1/namespaces/{namespace}/pods/{name}/nodeports": { - "get": { + "/v1/kthena/inference-templates/{id}": { + "put": { "security": [ { "Bearer": [] } ], - "description": "通过Pod的labels选择相关的Service并获取NodePort规则", + "description": "Replace a template owned by the current user in the active account.", "consumes": [ "application/json" ], @@ -7458,128 +7768,107 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的NodePort规则", + "summary": "Update a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", + "type": "integer", + "description": "Template ID", + "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true + "description": "Template", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateReq" + } } ], "responses": { "200": { - "description": "Pod NodePort规则列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp" } }, "400": { - "description": "请求参数错误", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "Pod未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "post": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "为指定Pod创建新的NodePort规则,规则名称和端口号必须唯一", - "consumes": [ - "application/json" - ], + "description": "Delete one template owned by the current user in the active account.", "produces": [ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "创建新的Pod NodePort规则", + "summary": "Delete a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", + "type": "integer", + "description": "Template ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "NodePort规则内容", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" - } } ], "responses": { "200": { - "description": "成功创建的NodePort规则", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport" - } - }, - "400": { - "description": "请求参数错误或规则冲突", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "404": { - "description": "Pod未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "delete": { + } + }, + "/v1/models/download": { + "post": { "security": [ { "Bearer": [] } ], - "description": "根据规则名称删除指定的NodePort规则,同时删除关联的Service", + "description": "创建一个新的模型下载任务", "consumes": [ "application/json" ], @@ -7587,70 +7876,38 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Pod" + "ModelDownload" ], - "summary": "删除Pod的NodePort规则", + "summary": "创建模型下载任务", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "要删除的NodePort规则", - "name": "body", + "description": "下载请求", + "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" + "$ref": "#/definitions/internal_handler.CreateDownloadReq" } } ], "responses": { "200": { - "description": "NodePort规则删除成功", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误或NodePort规则未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "404": { - "description": "Pod未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes": { + "/v1/models/downloads": { "get": { "security": [ { "Bearer": [] } ], - "description": "kubectl + prometheus获取节点基本信息", + "description": "下载记录对全平台用户可见。带 page 参数时返回分页结构(含状态汇总),否则返回全量数组(兼容旧客户端)", "consumes": [ "application/json" ], @@ -7658,39 +7915,59 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取节点基本信息", - "responses": { - "200": { - "description": "成功返回值描述,注意这里返回Json字符串,swagger无法准确解析", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } + "summary": "获取模型下载任务列表", + "parameters": [ + { + "type": "string", + "description": "过滤类别: model 或 dataset", + "name": "category", + "in": "query" }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } + { + "type": "integer", + "description": "页码(从1开始);不传则返回全量数组", + "name": "page", + "in": "query" }, - "500": { - "description": "其他错误", + { + "type": "integer", + "description": "每页数量,默认20,最大100", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "过滤状态: Pending/Downloading/Paused/Ready/Failed", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "按名称模糊搜索", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp" } } } } }, - "/v1/nodes/{name}": { + "/v1/models/downloads/{id}": { "get": { "security": [ { "Bearer": [] } ], - "description": "kubectl + prometheus获取节点详细信息", + "description": "根据 ID 获取模型下载任务详情", "consumes": [ "application/json" ], @@ -7698,46 +7975,34 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取节点详细信息", + "summary": "获取单个模型下载任务详情", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true } ], "responses": { "200": { - "description": "成功返回值", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } }, - "put": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "介绍函数的主要实现逻辑", + "description": "删除下载任务记录(仅平台管理员),已下载的文件保留在存储中", "consumes": [ "application/json" ], @@ -7745,57 +8010,36 @@ const docTemplate = `{ "application/json" ], "tags": [ - "接口对应的标签" + "ModelDownload" ], - "summary": "更新节点调度状态", + "summary": "删除模型下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "请求体,包含 reason 字段", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeScheduleRequest" - } } ], "responses": { "200": { - "description": "成功返回值", + "description": "OK", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } } } } }, - "/v1/nodes/{name}/annotation": { - "post": { + "/v1/models/downloads/{id}/logs": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加注解", + "description": "返回定时持久化到下载记录中的日志", "consumes": [ "application/json" ], @@ -7803,55 +8047,36 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "添加节点注解", + "summary": "获取模型下载任务日志", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "注解信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeAnnotation" - } } ], "responses": { "200": { - "description": "成功添加注解", + "description": "OK", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } } } - }, - "delete": { + } + }, + "/v1/models/downloads/{id}/pause": { + "post": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的注解", + "description": "暂停正在进行的模型下载任务", "consumes": [ "application/json" ], @@ -7859,57 +8084,36 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "删除节点注解", + "summary": "暂停下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "注解信息(只需要key)", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeAnnotation" - } } ], "responses": { "200": { - "description": "成功删除注解", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes/{name}/gpu/": { - "get": { + "/v1/models/downloads/{id}/resume": { + "post": { "security": [ { "Bearer": [] } ], - "description": "查询prometheus获取GPU各节点的利用率", + "description": "恢复已暂停的模型下载任务", "consumes": [ "application/json" ], @@ -7917,47 +8121,44 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取GPU各节点的利用率", + "summary": "恢复下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", - "in": "query" + "type": "integer", + "description": "下载任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "可选的临时访问令牌", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.DownloadActionReq" + } } ], "responses": { "200": { - "description": "成功返回值描述", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes/{name}/label": { + "/v1/models/downloads/{id}/retry": { "post": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加标签", + "description": "重新提交失败的模型下载任务", "consumes": [ "application/json" ], @@ -7965,55 +8166,44 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "添加节点标签", + "summary": "重试失败的下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true }, { - "description": "标签信息", + "description": "可选的临时访问令牌和重试版本", "name": "data", "in": "body", - "required": true, "schema": { - "$ref": "#/definitions/internal_handler.NodeLabel" + "$ref": "#/definitions/internal_handler.DownloadActionReq" } } ], "responses": { "200": { - "description": "成功添加标签", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } - }, - "delete": { + } + }, + "/v1/namespaces/{namespace}/pods/{name}/containers": { + "get": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的标签", + "description": "获取Pod的容器列表", "consumes": [ "application/json" ], @@ -8021,42 +8211,40 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "删除节点标签", + "summary": "获取Pod的容器列表", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "标签信息(只需要key)", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeLabel" - } + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功删除标签", + "description": "Pod容器列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8064,14 +8252,14 @@ const docTemplate = `{ } } }, - "/v1/nodes/{name}/mark": { + "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/log": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定节点的Labels、Annotations和Taints信息", + "description": "获取Pod容器日志", "consumes": [ "application/json" ], @@ -8079,33 +8267,61 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "获取节点标记信息", + "summary": "获取Pod容器日志", "parameters": [ { "type": "string", - "description": "节点名称", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", "name": "name", "in": "path", "required": true + }, + { + "type": "string", + "description": "容器名称", + "name": "container", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query", + "required": true } ], "responses": { "200": { - "description": "成功返回节点标记信息", + "description": "Pod容器日志", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8113,14 +8329,9 @@ const docTemplate = `{ } } }, - "/v1/nodes/{name}/pod/": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "kubectl + prometheus获取节点Pod信息", + "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/resources": { + "put": { + "description": "edit pod's resources(cpu, mem)", "consumes": [ "application/json" ], @@ -8128,32 +8339,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" - ], - "summary": "获取节点Pod信息", - "parameters": [ - { - "type": "string", - "description": "节点名称", - "name": "name", - "in": "query" - } + "Operations" ], + "summary": "edit pod's resources(cpu, mem)", "responses": { "200": { - "description": "成功返回值描述", + "description": "Success", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8161,14 +8364,14 @@ const docTemplate = `{ } } }, - "/v1/nodes/{name}/taint": { - "post": { + "/v1/namespaces/{namespace}/pods/{name}/events": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加污点", + "description": "获取Pod的事件", "consumes": [ "application/json" ], @@ -8176,32 +8379,30 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "添加节点污点", + "summary": "获取Pod的事件", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "污点信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeTaint" - } + "type": "string", + "description": "任务名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功添加污点", + "description": "Pod事件列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { @@ -8210,21 +8411,29 @@ const docTemplate = `{ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "500": { - "description": "其他错误", + "404": { + "description": "任务未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "delete": { + } + }, + "/v1/namespaces/{namespace}/pods/{name}/ingresses": { + "get": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的污点", + "description": "通过Pod注解获取相关的Ingress规则", "consumes": [ "application/json" ], @@ -8232,32 +8441,30 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "删除节点污点", + "summary": "获取Pod的Ingress规则", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "污点信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeTaint" - } + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功删除污点", + "description": "Pod Ingress规则列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp" } }, "400": { @@ -8266,6 +8473,12 @@ const docTemplate = `{ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, + "404": { + "description": "Pod未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, "500": { "description": "其他错误", "schema": { @@ -8273,11 +8486,14 @@ const docTemplate = `{ } } } - } - }, - "/v1/operations/add/locktime": { - "put": { - "description": "set LockTime of the job", + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "为指定Pod创建新的Ingress规则,规则名称和端口号必须唯一", "consumes": [ "application/json" ], @@ -8285,34 +8501,68 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "创建新的Pod Ingress规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "Ingress规则内容", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + } + } ], - "summary": "set LockTime of the job", "responses": { "200": { - "description": "Success", + "description": "成功创建的Ingress规则", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或规则冲突", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/operations/clear/locktime": { - "put": { - "description": "clear LockTime of the job", + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据规则名称删除指定的Ingress规则,同时删除关联的Service和Ingress", "consumes": [ "application/json" ], @@ -8320,24 +8570,55 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "删除Pod的Ingress规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "要删除的Ingress规则", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + } + } ], - "summary": "clear LockTime of the job", "responses": { "200": { - "description": "Success", + "description": "Ingress规则删除成功", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或Ingress规则未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8345,14 +8626,14 @@ const docTemplate = `{ } } }, - "/v1/operations/cronjob": { + "/v1/namespaces/{namespace}/pods/{name}/nodeports": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get all cronjob configs", + "description": "通过Pod的labels选择相关的Service并获取NodePort规则", "consumes": [ "application/json" ], @@ -8360,37 +8641,59 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "获取Pod的NodePort规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + } ], - "summary": "Get all cronjob configs", "responses": { "200": { - "description": "Success", + "description": "Pod NodePort规则列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "put": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Update one cronjob config", + "description": "为指定Pod创建新的NodePort规则,规则名称和端口号必须唯一", "consumes": [ "application/json" ], @@ -8398,45 +8701,68 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Pod" ], - "summary": "Update cronjob config", + "summary": "创建新的Pod NodePort规则", "parameters": [ { - "description": "CronjobConfig", - "name": "use", + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "NodePort规则内容", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfig" + "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" } } ], "responses": { "200": { - "description": "Success", + "description": "成功创建的NodePort规则", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或规则冲突", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/operations/keep/{name}": { - "put": { - "description": "set KeepWhenLowResourceUsage of the job to the opposite value", + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据规则名称删除指定的NodePort规则,同时删除关联的Service", "consumes": [ "application/json" ], @@ -8444,24 +8770,55 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Pod" ], - "summary": "set KeepWhenLowResourceUsage of the job to the opposite value", - "responses": { - "200": { - "description": "Success", + "summary": "删除Pod的NodePort规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "要删除的NodePort规则", + "name": "body", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" + } + } + ], + "responses": { + "200": { + "description": "NodePort规则删除成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或NodePort规则未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8469,9 +8826,14 @@ const docTemplate = `{ } } }, - "/v1/operations/whitelist": { + "/v1/nodes": { "get": { - "description": "get job white list", + "security": [ + { + "Bearer": [] + } + ], + "description": "kubectl + prometheus获取节点基本信息", "consumes": [ "application/json" ], @@ -8479,24 +8841,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Operations" + "Node" ], - "summary": "Get job white list", + "summary": "获取节点基本信息", "responses": { "200": { - "description": "Success", + "description": "成功返回值描述,注意这里返回Json字符串,swagger无法准确解析", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8504,14 +8866,14 @@ const docTemplate = `{ } } }, - "/v1/projects": { - "post": { + "/v1/nodes/{name}": { + "get": { "security": [ { "Bearer": [] } ], - "description": "从请求中获取账户名称、描述和配额,以当前用户为管理员,创建一个团队账户", + "description": "kubectl + prometheus获取节点详细信息", "consumes": [ "application/json" ], @@ -8519,23 +8881,23 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Project" + "Node" ], - "summary": "创建团队账户", + "summary": "获取节点详细信息", "parameters": [ { - "description": "账户信息", - "name": "data", - "in": "body", - "required": true, - "schema": {} + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功创建账户,返回账户ID", + "description": "成功返回值", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail" } }, "400": { @@ -8545,22 +8907,20 @@ const docTemplate = `{ } }, "500": { - "description": "账户创建失败,返回错误信息", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/resources": { - "get": { + }, + "put": { "security": [ { "Bearer": [] } ], - "description": "If the vendorDomain parameter is provided, the API will return a list of resources that match the specified vendor domain.", + "description": "介绍函数的主要实现逻辑", "consumes": [ "application/json" ], @@ -8568,32 +8928,42 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Resource" + "接口对应的标签" ], - "summary": "Get a list of resources based on the specified parameters", + "summary": "更新节点调度状态", "parameters": [ { "type": "string", - "description": "Vendor domain of the resource (For example: 'nvidia.com'\t)", - "name": "vendorDomain", - "in": "query" + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "请求体,包含 reason 字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeScheduleRequest" + } } ], "responses": { "200": { - "description": "Success", + "description": "成功返回值", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8601,14 +8971,14 @@ const docTemplate = `{ } } }, - "/v1/resources/gpu/{gpuId}/networks": { - "get": { + "/v1/nodes/{name}/annotation": { + "post": { "security": [ { "Bearer": [] } ], - "description": "This API will return all RDMA resources linked to the specified GPU resource.", + "description": "为指定节点添加注解", "consumes": [ "application/json" ], @@ -8616,48 +8986,55 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Resource" + "Node" ], - "summary": "Get all RDMA resources linked to a GPU resource", + "summary": "添加节点注解", "parameters": [ { - "type": "integer", - "description": "GPU Resource ID", - "name": "gpuId", + "type": "string", + "description": "节点名称", + "name": "name", "in": "path", "required": true + }, + { + "description": "注解信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeAnnotation" + } } ], "responses": { "200": { - "description": "Success", + "description": "成功添加注解", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/spjobs/{name}/events": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "获取稀疏推荐作业关联的事件信息", + "description": "删除指定节点的注解", "consumes": [ "application/json" ], @@ -8665,23 +9042,32 @@ const docTemplate = `{ "application/json" ], "tags": [ - "SpJob" + "Node" ], - "summary": "获取稀疏推荐作业的事件", + "summary": "删除节点注解", "parameters": [ { "type": "string", - "description": "Job Name", + "description": "节点名称", "name": "name", "in": "path", "required": true + }, + { + "description": "注解信息(只需要key)", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeAnnotation" + } } ], "responses": { "200": { - "description": "事件列表", + "description": "成功删除注解", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8699,14 +9085,14 @@ const docTemplate = `{ } } }, - "/v1/statistics": { + "/v1/nodes/{name}/gpu/": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定时间范围、指定维度的资源使用统计(核时/卡时)", + "description": "查询prometheus获取GPU各节点的利用率", "consumes": [ "application/json" ], @@ -8714,103 +9100,80 @@ const docTemplate = `{ "application/json" ], "tags": [ - "statistics" + "Node" ], - "summary": "获取资源统计信息", + "summary": "获取GPU各节点的利用率", "parameters": [ { "type": "string", - "description": "开始时间 (RFC3339)", - "name": "startTime", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "结束时间 (RFC3339)", - "name": "endTime", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "聚合粒度 (day/week)", - "name": "step", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "统计范围 (user/account/cluster)", - "name": "scope", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "目标ID (user_id 或 account_id)", - "name": "targetID", + "description": "节点名称", + "name": "name", "in": "query" } ], "responses": { "200": { - "description": "OK", + "description": "成功返回值描述", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/system-config/model-download-limit": { - "get": { + "/v1/nodes/{name}/label": { + "post": { "security": [ { "Bearer": [] } ], - "description": "获取当前用户的并发任务上限、滚动窗口成功下载上限和白名单豁免状态", + "description": "为指定节点添加标签", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "SystemConfig" + "Node" ], - "summary": "获取模型与数据集下载额度", - "responses": { - "200": { - "description": "OK", + "summary": "添加节点标签", + "parameters": [ + { + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "标签信息", + "name": "data", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp" + "$ref": "#/definitions/internal_handler.NodeLabel" } } - } - } - }, - "/v1/token/verify": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "读取header的auth进行鉴权", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" ], - "tags": [ - "Token" - ], - "summary": "通过token鉴权", "responses": { "200": { - "description": "Token 鉴权", + "description": "成功添加标签", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8826,56 +9189,47 @@ const docTemplate = `{ } } } - } - }, - "/v1/users/ban": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "返回当前用户仍在生效的封禁时间、原因和限制内容,不包含管理员操作历史", + "description": "删除指定节点的标签", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取当前用户封禁状态", - "responses": { - "200": { - "description": "OK", + "summary": "删除节点标签", + "parameters": [ + { + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "标签信息(只需要key)", + "name": "data", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp" + "$ref": "#/definitions/internal_handler.NodeLabel" } } - } - } - }, - "/v1/users/email/verified": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "检查邮箱是否已验证", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" ], - "summary": "检查邮箱是否已验证", "responses": { "200": { - "description": "成功获取用户信息", + "description": "成功删除标签", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8893,14 +9247,14 @@ const docTemplate = `{ } } }, - "/v1/users/{name}": { + "/v1/nodes/{name}/mark": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定用户的详细信息", + "description": "获取指定节点的Labels、Annotations和Taints信息", "consumes": [ "application/json" ], @@ -8908,13 +9262,13 @@ const docTemplate = `{ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取单个用户信息", + "summary": "获取节点标记信息", "parameters": [ { "type": "string", - "description": "username", + "description": "节点名称", "name": "name", "in": "path", "required": true @@ -8922,9 +9276,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "成功获取用户信息", + "description": "成功返回节点标记信息", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark" } }, "400": { @@ -8942,48 +9296,62 @@ const docTemplate = `{ } } }, - "/v1/users/{name}/ban": { + "/v1/nodes/{name}/pod/": { "get": { "security": [ { "Bearer": [] } ], - "description": "已登录用户可查看指定用户的封禁状态和记录,但不返回执行管理员信息", + "description": "kubectl + prometheus获取节点Pod信息", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取用户封禁状态和记录", + "summary": "获取节点Pod信息", "parameters": [ { "type": "string", - "description": "username", + "description": "节点名称", "name": "name", - "in": "path", - "required": true + "in": "query" } ], "responses": { "200": { - "description": "OK", + "description": "成功返回值描述", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs": { - "get": { + "/v1/nodes/{name}/taint": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Get the jobs of the user by client-go", + "description": "为指定节点添加污点", "consumes": [ "application/json" ], @@ -8991,107 +9359,55 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Node" ], - "summary": "Get the jobs of the user", + "summary": "添加节点污点", "parameters": [ - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, { "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" + "description": "节点名称", + "name": "name", + "in": "path", + "required": true }, { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" + "description": "污点信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeTaint" + } } ], "responses": { "200": { - "description": "Volcano Job List", + "description": "成功添加污点", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/vcjobs/all": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "返回指定天数内的所有作业,默认为14天", + "description": "删除指定节点的污点", "consumes": [ "application/json" ], @@ -9099,93 +9415,42 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Node" ], - "summary": "Get all of the jobs", + "summary": "删除节点污点", "parameters": [ { - "type": "integer", - "default": 14, - "description": "Number of days to look back, default is 14", - "name": "days", - "in": "query" + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true }, { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" + "description": "污点信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeTaint" + } } ], "responses": { "200": { - "description": "admin get Volcano Job List", + "description": "成功删除污点", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "admin Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9193,162 +9458,44 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/all/facets": { - "get": { - "security": [ - { - "Bearer": [] - } + "/v1/operations/add/locktime": { + "put": { + "description": "set LockTime of the job", + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get all visible job facets", - "parameters": [ - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "set LockTime of the job", "responses": { "200": { - "description": "OK", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } - } - } - } - }, - "/v1/vcjobs/facets": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "VolcanoJob" - ], - "summary": "Get job facets for the current user", - "parameters": [ - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", + "500": { + "description": "Other errors", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs/jupyter": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Create a Jupyter job", + "/v1/operations/clear/locktime": { + "put": { + "description": "clear LockTime of the job", "consumes": [ "application/json" ], @@ -9356,20 +9503,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a Jupyter job", - "parameters": [ - { - "description": "Create Jupyter Job Request", - "name": "CreateJupyterReq", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" - } - } + "Operations" ], + "summary": "clear LockTime of the job", "responses": { "200": { "description": "Success", @@ -9392,14 +9528,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/tensorflow": { - "post": { + "/v1/operations/cronjob": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Create a training job", + "description": "Get all cronjob configs", "consumes": [ "application/json" ], @@ -9407,18 +9543,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a training job", - "parameters": [ - { - "description": "CreateTrainingReq", - "name": "CreateTrainingReq", - "in": "body", - "required": true, - "schema": {} - } + "Operations" ], + "summary": "Get all cronjob configs", "responses": { "200": { "description": "Success", @@ -9439,16 +9566,14 @@ const docTemplate = `{ } } } - } - }, - "/v1/vcjobs/training": { - "post": { + }, + "put": { "security": [ { "Bearer": [] } ], - "description": "Create a training job", + "description": "Update one cronjob config", "consumes": [ "application/json" ], @@ -9456,16 +9581,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Operations" ], - "summary": "Create a training job", + "summary": "Update cronjob config", "parameters": [ { - "description": "CreateTrainingReq", - "name": "CreateTrainingReq", + "description": "CronjobConfig", + "name": "use", "in": "body", "required": true, - "schema": {} + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfig" + } } ], "responses": { @@ -9490,14 +9617,9 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/user/{username}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get job list of a specific user within specified days. Both users and administrators can call this API.", + "/v1/operations/keep/{name}": { + "put": { + "description": "set KeepWhenLowResourceUsage of the job to the opposite value", "consumes": [ "application/json" ], @@ -9505,90 +9627,14 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get jobs of a specific user within days", - "parameters": [ - { - "type": "string", - "description": "Username", - "name": "username", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 30, - "description": "Number of days to look back, default is 30, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "set KeepWhenLowResourceUsage of the job to the opposite value", "responses": { "200": { - "description": "User's Job List", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { @@ -9597,18 +9643,6 @@ const docTemplate = `{ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "403": { - "description": "Forbidden - insufficient permissions", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "404": { - "description": "User not found", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, "500": { "description": "Other errors", "schema": { @@ -9618,95 +9652,49 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/user/{username}/facets": { + "/v1/operations/whitelist": { "get": { - "security": [ - { - "Bearer": [] - } + "description": "get job white list", + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get job facets for a user", - "parameters": [ - { - "type": "string", - "description": "Username", - "name": "username", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "Get job white list", "responses": { "200": { - "description": "OK", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs/webide": { + "/v1/projects": { "post": { "security": [ { "Bearer": [] } ], - "description": "Create a WebIDE job", + "description": "从请求中获取账户名称、描述和配额,以当前用户为管理员,创建一个团队账户", "consumes": [ "application/json" ], @@ -9714,35 +9702,33 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Project" ], - "summary": "Create a WebIDE job", + "summary": "创建团队账户", "parameters": [ { - "description": "Create WebIDE Job Request", - "name": "CreateJupyterReq", + "description": "账户信息", + "name": "data", "in": "body", "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" - } + "schema": {} } ], "responses": { "200": { - "description": "Success", + "description": "成功创建账户,返回账户ID", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "账户创建失败,返回错误信息", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9750,14 +9736,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}": { - "delete": { + "/v1/resources": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Delete the job by client-go", + "description": "If the vendorDomain parameter is provided, the API will return a list of resources that match the specified vendor domain.", "consumes": [ "application/json" ], @@ -9765,16 +9751,15 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Resource" ], - "summary": "Delete the job", + "summary": "Get a list of resources based on the specified parameters", "parameters": [ { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "description": "Vendor domain of the resource (For example: 'nvidia.com'\t)", + "name": "vendorDomain", + "in": "query" } ], "responses": { @@ -9799,9 +9784,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/alert": { - "put": { - "description": "set AlertEnabled of the job to the opposite value", + "/v1/resources/gpu/{gpuId}/networks": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "This API will return all RDMA resources linked to the specified GPU resource.", "consumes": [ "application/json" ], @@ -9809,9 +9799,18 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "Resource" + ], + "summary": "Get all RDMA resources linked to a GPU resource", + "parameters": [ + { + "type": "integer", + "description": "GPU Resource ID", + "name": "gpuId", + "in": "path", + "required": true + } ], - "summary": "set AlertEnabled of the job to the opposite value", "responses": { "200": { "description": "Success", @@ -9834,14 +9833,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/detail": { + "/v1/spjobs/{name}/events": { "get": { "security": [ { "Bearer": [] } ], - "description": "调用k8s get crd", + "description": "获取稀疏推荐作业关联的事件信息", "consumes": [ "application/json" ], @@ -9849,9 +9848,9 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "SpJob" ], - "summary": "获取jupyter详情", + "summary": "获取稀疏推荐作业的事件", "parameters": [ { "type": "string", @@ -9863,19 +9862,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "任务描述", + "description": "事件列表", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9883,14 +9882,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/event": { + "/v1/statistics": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的事件", + "description": "获取指定时间范围、指定维度的资源使用统计(核时/卡时)", "consumes": [ "application/json" ], @@ -9898,97 +9897,113 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "statistics" ], - "summary": "获取任务的事件", + "summary": "获取资源统计信息", "parameters": [ { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", + "description": "开始时间 (RFC3339)", + "name": "startTime", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "结束时间 (RFC3339)", + "name": "endTime", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "聚合粒度 (day/week)", + "name": "step", + "in": "query", "required": true + }, + { + "type": "string", + "description": "统计范围 (user/account/cluster)", + "name": "scope", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "目标ID (user_id 或 account_id)", + "name": "targetID", + "in": "query" } ], "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp" } } } } }, - "/v1/vcjobs/{name}/pods": { + "/v1/system-config/kthena-inference": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的Pod列表", - "consumes": [ - "application/json" - ], + "description": "查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。", "produces": [ "application/json" ], "tags": [ - "VolcanoJob" + "SystemConfig" ], - "summary": "获取任务的Pod列表", - "parameters": [ + "summary": "获取模型部署功能开关状态", + "responses": { + "200": { + "description": "开关状态", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp" + } + } + } + } + }, + "/v1/system-config/model-download-limit": { + "get": { + "security": [ { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "Bearer": [] } ], + "description": "获取当前用户的并发任务上限、滚动窗口成功下载上限和白名单豁免状态", + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "获取模型与数据集下载额度", "responses": { "200": { - "description": "Pod列表", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp" } } } } }, - "/v1/vcjobs/{name}/secret": { + "/v1/token/verify": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get the password of the WebIDE job by reading config file in the pod", + "description": "读取header的auth进行鉴权", "consumes": [ "application/json" ], @@ -9996,33 +10011,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get the password of the WebIDE job", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "Token" ], + "summary": "通过token鉴权", "responses": { "200": { - "description": "Success", + "description": "Token 鉴权", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10030,63 +10036,39 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/snapshot": { - "post": { + "/v1/users/ban": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Create nerdctl docker commit to snapshot the job container (supports Jupyter and Custom job types)", - "consumes": [ - "application/json" - ], + "description": "返回当前用户仍在生效的封禁时间、原因和限制内容,不包含管理员操作历史", "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a snapshot of the job container", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "User" ], + "summary": "获取当前用户封禁状态", "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp" } } } } }, - "/v1/vcjobs/{name}/ssh": { - "post": { + "/v1/users/email/verified": { + "get": { "security": [ { "Bearer": [] } ], - "description": "开启 SSH", + "description": "检查邮箱是否已验证", "consumes": [ "application/json" ], @@ -10094,33 +10076,24 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "开启 SSH", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "User" ], + "summary": "检查邮箱是否已验证", "responses": { "200": { - "description": "SSH开启成功", + "description": "成功获取用户信息", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10128,14 +10101,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/template": { + "/v1/users/{name}": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的 template", + "description": "获取指定用户的详细信息", "consumes": [ "application/json" ], @@ -10143,13 +10116,13 @@ const docTemplate = `{ "application/json" ], "tags": [ - "VolcanoJob" + "User" ], - "summary": "获取任务的 template", + "summary": "获取单个用户信息", "parameters": [ { "type": "string", - "description": "Job Name", + "description": "username", "name": "name", "in": "path", "required": true @@ -10157,19 +10130,19 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Success", + "description": "成功获取用户信息", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10177,14 +10150,48 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/token": { + "/v1/users/{name}/ban": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get the token of the job by logs", + "description": "已登录用户可查看指定用户的封禁状态和记录,但不返回执行管理员信息", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "获取用户封禁状态和记录", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp" + } + } + } + } + }, + "/v1/vcjobs": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the jobs of the user by client-go", "consumes": [ "application/json" ], @@ -10194,21 +10201,80 @@ const docTemplate = `{ "tags": [ "VolcanoJob" ], - "summary": "Get the ingress base url and jupyter token of the job", + "summary": "Get the jobs of the user", "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, { "type": "string", - "description": "Job Name", - "name": "jobName", - "in": "path", - "required": true + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" } ], "responses": { "200": { - "description": "Success", + "description": "Volcano Job List", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" } }, "400": { @@ -10226,14 +10292,14 @@ const docTemplate = `{ } } }, - "/v1/vcjobs/{name}/yaml": { + "/v1/vcjobs/all": { "get": { "security": [ { "Bearer": [] } ], - "description": "调用k8s get crd", + "description": "返回指定天数内的所有作业,默认为14天", "consumes": [ "application/json" ], @@ -10241,27 +10307,87 @@ const docTemplate = `{ "application/json" ], "tags": [ - "vcjob-jupyter" + "VolcanoJob" ], - "summary": "获取vcjob Yaml详情", + "summary": "Get all of the jobs", "parameters": [ + { + "type": "integer", + "default": 14, + "description": "Number of days to look back, default is 14", + "name": "days", + "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" } ], "responses": { "200": { - "description": "任务yaml", + "description": "admin get Volcano Job List", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" } }, "400": { - "description": "Request parameter error", + "description": "admin Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10274,618 +10400,2336 @@ const docTemplate = `{ } } } - } - }, - "definitions": { - "datatypes.JSONType-array_string": { - "type": "object" - }, - "datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota": { - "type": "object" - }, - "datatypes.JSONType-v1_ResourceList": { - "type": "object" - }, - "github_com_raids-lab_crater_dao_model.AccessMode": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "x-enum-comments": { - "AccessModeAO": "Append-only mode", - "AccessModeNA": "Not-allowed mode", - "AccessModeRO": "Read-only mode", - "AccessModeRW": "Read-write mode" - }, - "x-enum-descriptions": [ - "", - "Not-allowed mode", - "Read-only mode", - "Read-write mode", - "Append-only mode" - ], - "x-enum-varnames": [ - "_", - "AccessModeNA", - "AccessModeRO", - "AccessModeRW", - "AccessModeAO" - ] - }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderContent": { - "type": "object", - "properties": { - "approvalorderExtensionHours": { - "description": "延长小时数", - "type": "integer" - }, - "approvalorderReason": { - "description": "审批原因", - "type": "string" - }, - "approvalorderTypeID": { - "type": "integer" - } - } }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderStatus": { - "type": "string", - "enum": [ + "/v1/vcjobs/all/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get all visible job facets", + "parameters": [ + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get job facets for the current user", + "parameters": [ + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/jupyter": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a Jupyter job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a Jupyter job", + "parameters": [ + { + "description": "Create Jupyter Job Request", + "name": "CreateJupyterReq", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/tensorflow": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a training job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a training job", + "parameters": [ + { + "description": "CreateTrainingReq", + "name": "CreateTrainingReq", + "in": "body", + "required": true, + "schema": {} + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/training": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a training job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a training job", + "parameters": [ + { + "description": "CreateTrainingReq", + "name": "CreateTrainingReq", + "in": "body", + "required": true, + "schema": {} + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/user/{username}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get job list of a specific user within specified days. Both users and administrators can call this API.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get jobs of a specific user within days", + "parameters": [ + { + "type": "string", + "description": "Username", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 30, + "description": "Number of days to look back, default is 30, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "User's Job List", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "User not found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/user/{username}/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get job facets for a user", + "parameters": [ + { + "type": "string", + "description": "Username", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/webide": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a WebIDE job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a WebIDE job", + "parameters": [ + { + "description": "Create WebIDE Job Request", + "name": "CreateJupyterReq", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/workloads": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Lists persisted Volcano jobs and current-user Kthena ModelBoosters as a single pageable list.", + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get current user's unified workloads", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search workloads", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types, including model-deployment", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Workload kinds", + "name": "workload_kind", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Workload statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp" + } + } + } + } + }, + "/v1/vcjobs/workloads/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get current user's unified workload facets", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/{name}": { + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete the job by client-go", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Delete the job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/alert": { + "put": { + "description": "set AlertEnabled of the job to the opposite value", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "set AlertEnabled of the job to the opposite value", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/detail": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "调用k8s get crd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取jupyter详情", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务描述", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/event": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的事件", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/pods": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的Pod列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的Pod列表", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pod列表", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/secret": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the password of the WebIDE job by reading config file in the pod", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get the password of the WebIDE job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/snapshot": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create nerdctl docker commit to snapshot the job container (supports Jupyter and Custom job types)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a snapshot of the job container", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/ssh": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "开启 SSH", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "开启 SSH", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "SSH开启成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/template": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的 template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的 template", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/token": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the token of the job by logs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get the ingress base url and jupyter token of the job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "jobName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/yaml": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "调用k8s get crd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "vcjob-jupyter" + ], + "summary": "获取vcjob Yaml详情", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务yaml", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + } + }, + "definitions": { + "datatypes.JSONType-array_string": { + "type": "object" + }, + "datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota": { + "type": "object" + }, + "datatypes.JSONType-v1_ResourceList": { + "type": "object" + }, + "github_com_raids-lab_crater_dao_model.AccessMode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-comments": { + "AccessModeAO": "Append-only mode", + "AccessModeNA": "Not-allowed mode", + "AccessModeRO": "Read-only mode", + "AccessModeRW": "Read-write mode" + }, + "x-enum-descriptions": [ + "", + "Not-allowed mode", + "Read-only mode", + "Read-write mode", + "Append-only mode" + ], + "x-enum-varnames": [ + "_", + "AccessModeNA", + "AccessModeRO", + "AccessModeRW", + "AccessModeAO" + ] + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderContent": { + "type": "object", + "properties": { + "approvalorderExtensionHours": { + "description": "延长小时数", + "type": "integer" + }, + "approvalorderReason": { + "description": "审批原因", + "type": "string" + }, + "approvalorderTypeID": { + "type": "integer" + } + } + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderStatus": { + "type": "string", + "enum": [ "Pending", "Approved", "Rejected", "Canceled" ], "x-enum-comments": { - "ApprovalOrderStatusApproved": "已批准", - "ApprovalOrderStatusCancelled": "已取消", - "ApprovalOrderStatusPending": "待审批", - "ApprovalOrderStatusRejected": "已拒绝" + "ApprovalOrderStatusApproved": "已批准", + "ApprovalOrderStatusCancelled": "已取消", + "ApprovalOrderStatusPending": "待审批", + "ApprovalOrderStatusRejected": "已拒绝" + }, + "x-enum-descriptions": [ + "待审批", + "已批准", + "已拒绝", + "已取消" + ], + "x-enum-varnames": [ + "ApprovalOrderStatusPending", + "ApprovalOrderStatusApproved", + "ApprovalOrderStatusRejected", + "ApprovalOrderStatusCancelled" + ] + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderType": { + "type": "string", + "enum": [ + "dataset", + "job" + ], + "x-enum-comments": { + "ApprovalOrderTypeDataset": "数据集类型", + "ApprovalOrderTypeJob": "任务类型" + }, + "x-enum-descriptions": [ + "数据集类型", + "任务类型" + ], + "x-enum-varnames": [ + "ApprovalOrderTypeDataset", + "ApprovalOrderTypeJob" + ] + }, + "github_com_raids-lab_crater_dao_model.BuildSource": { + "type": "string", + "enum": [ + "Dockerfile", + "PipApt", + "Snapshot", + "EnvdAdvanced", + "EnvdRaw" + ], + "x-enum-varnames": [ + "Dockerfile", + "PipApt", + "Snapshot", + "EnvdAdvanced", + "EnvdRaw" + ] + }, + "github_com_raids-lab_crater_dao_model.CraterResourceType": { + "type": "string", + "enum": [ + "gpu", + "rdma", + "vgpu" + ], + "x-enum-varnames": [ + "ResourceTypeGPU", + "ResourceTypeRDMA", + "ResourceTypeVGPU" + ] + }, + "github_com_raids-lab_crater_dao_model.CronJobConfig": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "entry_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfigStatus" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobType" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.CronJobConfigStatus": { + "type": "string", + "enum": [ + "unknown", + "suspended", + "idle", + "running" + ], + "x-enum-varnames": [ + "CronJobConfigStatusUnknown", + "CronJobConfigStatusSuspended", + "CronJobConfigStatusIdle", + "CronJobConfigStatusRunning" + ] + }, + "github_com_raids-lab_crater_dao_model.CronJobType": { + "type": "string", + "enum": [ + "cleaner_function", + "patrol_function" + ], + "x-enum-varnames": [ + "CronJobTypeCleanerFunc", + "CronJobTypePatrolFunc" + ] + }, + "github_com_raids-lab_crater_dao_model.DataType": { + "type": "string", + "enum": [ + "dataset", + "model", + "sharefile" + ], + "x-enum-varnames": [ + "DataTypeDataset", + "DataTypeModel", + "DataTypeShareFile" + ] + }, + "github_com_raids-lab_crater_dao_model.GpuAnalysis": { + "type": "object", + "properties": { + "command": { + "description": "采集到的原始数据", + "type": "string" + }, + "createdAt": { + "description": "自动追踪的时间戳", + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "historicalMetrics": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "jobID": { + "type": "integer" + }, + "jobName": { + "type": "string" + }, + "llmversion": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "phase1LLMReason": { + "type": "string" + }, + "phase1Score": { + "description": "LLM 分析结果", + "type": "integer" + }, + "phase2LLMReason": { + "type": "string" + }, + "phase2Score": { + "type": "integer" + }, + "podName": { + "description": "原始 Kubernetes 信息", + "type": "string" + }, + "reviewStatus": { + "description": "管理状态", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" + } + ] + }, + "userID": { + "type": "integer" + }, + "userName": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.JobType": { + "type": "string", + "enum": [ + "all", + "jupyter", + "webide", + "pytorch", + "tensorflow", + "kuberay", + "deepspeed", + "openmpi", + "custom" + ], + "x-enum-varnames": [ + "JobTypeAll", + "JobTypeJupyter", + "JobTypeWebIDE", + "JobTypePytorch", + "JobTypeTensorflow", + "JobTypeKubeRay", + "JobTypeDeepSpeed", + "JobTypeOpenMPI", + "JobTypeCustom" + ] + }, + "github_com_raids-lab_crater_dao_model.ReviewStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "ReviewStatusConfirmed": "已确认", + "ReviewStatusIgnored": "已忽略", + "ReviewStatusPending": "待审核", + "_": "零值,被忽略" }, "x-enum-descriptions": [ - "待审批", - "已批准", - "已拒绝", - "已取消" + "零值,被忽略", + "待审核", + "已确认", + "已忽略" ], "x-enum-varnames": [ - "ApprovalOrderStatusPending", - "ApprovalOrderStatusApproved", - "ApprovalOrderStatusRejected", - "ApprovalOrderStatusCancelled" + "_", + "ReviewStatusPending", + "ReviewStatusConfirmed", + "ReviewStatusIgnored" + ] + }, + "github_com_raids-lab_crater_dao_model.Role": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "_", + "RoleGuest", + "RoleUser", + "RoleAdmin" + ] + }, + "github_com_raids-lab_crater_dao_model.ScheduleType": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ScheduleTypeBackfill", + "ScheduleTypeNormal" + ] + }, + "github_com_raids-lab_crater_dao_model.Status": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "StatusActive": "Active status", + "StatusInactive": "Inactive status", + "StatusPending": "Pending status, not yet activated" + }, + "x-enum-descriptions": [ + "", + "Pending status, not yet activated", + "Active status", + "Inactive status" + ], + "x-enum-varnames": [ + "_", + "StatusPending", + "StatusActive", + "StatusInactive" + ] + }, + "github_com_raids-lab_crater_dao_model.UserAttribute": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "email": { + "description": "邮箱", + "type": "string" + }, + "expiredAt": { + "description": "过期时间", + "type": "string" + }, + "gid": { + "description": "GID", + "type": "string" + }, + "group": { + "description": "课题组", + "type": "string" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "name": { + "description": "账号", + "type": "string" + }, + "nickname": { + "description": "昵称,如果没有指定,则与账号相同", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "teacher": { + "description": "老师", + "type": "string" + }, + "uid": { + "description": "UID and GID are used for Filesystem", + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.UserBanAction": { + "type": "string", + "enum": [ + "ban", + "extend", + "unban" + ], + "x-enum-varnames": [ + "UserBanActionBan", + "UserBanActionExtend", + "UserBanActionUnban" + ] + }, + "github_com_raids-lab_crater_dao_model.UserBanRestrictions": { + "type": "object", + "properties": { + "datasetDownload": { + "type": "boolean" + }, + "imageBuild": { + "type": "boolean" + }, + "jobSubmission": { + "type": "boolean" + }, + "modelDownload": { + "type": "boolean" + }, + "platformAccess": { + "type": "boolean" + } + } + }, + "github_com_raids-lab_crater_dao_model.UserInfo": { + "type": "object", + "properties": { + "nickname": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_payload.Order": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "x-enum-varnames": [ + "Asc", + "Desc" ] }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderType": { - "type": "string", - "enum": [ - "dataset", - "job" - ], - "x-enum-comments": { - "ApprovalOrderTypeDataset": "数据集类型", - "ApprovalOrderTypeJob": "任务类型" - }, - "x-enum-descriptions": [ - "数据集类型", - "任务类型" - ], - "x-enum-varnames": [ - "ApprovalOrderTypeDataset", - "ApprovalOrderTypeJob" - ] + "github_com_raids-lab_crater_internal_payload.ResourceDetail": { + "type": "object", + "properties": { + "label": { + "description": "显示名称 (例如 \"NVIDIA V100\", \"CPU\", \"内存\")", + "type": "string" + }, + "type": { + "description": "资源类型 (gpu, vgpu, rdma, common)", + "type": "string" + }, + "usage": { + "description": "用量 (核时/卡时/GiB时)", + "type": "number" + } + } + }, + "github_com_raids-lab_crater_internal_payload.StatisticsResp": { + "type": "object", + "properties": { + "series": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.TimePointData" + } + }, + "totalUsage": { + "description": "TotalUsage Key: ResourceName (如 nvidia.com/v100)\nValue: 包含 Label 和 Type 的详细对象", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.ResourceDetail" + } + } + } + }, + "github_com_raids-lab_crater_internal_payload.TimePointData": { + "type": "object", + "properties": { + "timestamp": { + "type": "string" + }, + "usage": { + "description": "Key: ResourceName, Value: Usage", + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float64" + } + } + } + }, + "github_com_raids-lab_crater_internal_resputil.FacetItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "value": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.BuildSource": { - "type": "string", - "enum": [ - "Dockerfile", - "PipApt", - "Snapshot", - "EnvdAdvanced", - "EnvdRaw" - ], - "x-enum-varnames": [ - "Dockerfile", - "PipApt", - "Snapshot", - "EnvdAdvanced", - "EnvdRaw" - ] + "github_com_raids-lab_crater_internal_resputil.FacetResponse": { + "type": "object", + "properties": { + "facets": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetItem" + } + } + } + } }, - "github_com_raids-lab_crater_dao_model.CraterResourceType": { - "type": "string", - "enum": [ - "gpu", - "rdma", - "vgpu" - ], - "x-enum-varnames": [ - "ResourceTypeGPU", - "ResourceTypeRDMA", - "ResourceTypeVGPU" - ] + "github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler_operations.OperationLogResp" + } + }, + "total": { + "type": "integer" + } + } }, - "github_com_raids-lab_crater_dao_model.CronJobConfig": { + "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp": { "type": "object", "properties": { - "config": { + "items": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/internal_handler_vcjob.JobResp" } }, - "createdAt": { - "type": "string" + "page": { + "type": "integer" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "page_size": { + "type": "integer" }, - "entry_id": { + "total": { "type": "integer" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler_vcjob.WorkloadResp" + } }, - "id": { + "page": { "type": "integer" }, - "name": { + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-any": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": {}, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_github_com_raids-lab_crater_pkg_crclient_Pod": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "spec": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.Pod" + } + }, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_AccountResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfigStatus" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.AccountResp" + } }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobType" + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ApprovalOrderResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "updatedAt": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + } + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.CronJobConfigStatus": { - "type": "string", - "enum": [ - "unknown", - "suspended", - "idle", - "running" - ], - "x-enum-varnames": [ - "CronJobConfigStatusUnknown", - "CronJobConfigStatusSuspended", - "CronJobConfigStatusIdle", - "CronJobConfigStatusRunning" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_GpuAnalysisWithJobInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.GpuAnalysisWithJobInfo" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.CronJobType": { - "type": "string", - "enum": [ - "cleaner_function", - "patrol_function" - ], - "x-enum-varnames": [ - "CronJobTypeCleanerFunc", - "CronJobTypePatrolFunc" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.DataType": { - "type": "string", - "enum": [ - "dataset", - "model", - "sharefile" - ], - "x-enum-varnames": [ - "DataTypeDataset", - "DataTypeModel", - "DataTypeShareFile" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateResp" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.GpuAnalysis": { + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp": { "type": "object", "properties": { - "command": { - "description": "采集到的原始数据", - "type": "string" - }, - "createdAt": { - "description": "自动追踪的时间戳", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaServiceResp" + } }, - "historicalMetrics": { + "msg": { "type": "string" - }, - "id": { + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ModelDownloadResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "jobID": { - "type": "integer" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ModelDownloadResp" + } }, - "jobName": { + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_dao_model_GpuAnalysis": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "llmversion": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.GpuAnalysis" }, - "namespace": { + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "phase1LLMReason": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.StatisticsResp" }, - "phase1Score": { - "description": "LLM 分析结果", + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "phase2LLMReason": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetResponse" }, - "phase2Score": { + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_List-internal_handler_operations_OperationLogResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "podName": { - "description": "原始 Kubernetes 信息", + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp" + }, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "reviewStatus": { - "description": "管理状态", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" - } - ] + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp" }, - "userID": { + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "userName": { + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.JobType": { - "type": "string", - "enum": [ - "all", - "jupyter", - "webide", - "pytorch", - "tensorflow", - "kuberay", - "deepspeed", - "openmpi", - "custom" - ], - "x-enum-varnames": [ - "JobTypeAll", - "JobTypeJupyter", - "JobTypeWebIDE", - "JobTypePytorch", - "JobTypeTensorflow", - "JobTypeKubeRay", - "JobTypeDeepSpeed", - "JobTypeOpenMPI", - "JobTypeCustom" - ] - }, - "github_com_raids-lab_crater_dao_model.ReviewStatus": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "ReviewStatusConfirmed": "已确认", - "ReviewStatusIgnored": "已忽略", - "ReviewStatusPending": "待审核", - "_": "零值,被忽略" - }, - "x-enum-descriptions": [ - "零值,被忽略", - "待审核", - "已确认", - "已忽略" - ], - "x-enum-varnames": [ - "_", - "ReviewStatusPending", - "ReviewStatusConfirmed", - "ReviewStatusIgnored" - ] - }, - "github_com_raids-lab_crater_dao_model.Role": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "_", - "RoleGuest", - "RoleUser", - "RoleAdmin" - ] - }, - "github_com_raids-lab_crater_dao_model.ScheduleType": { - "type": "integer", - "enum": [ - 0, - 1 - ], - "x-enum-varnames": [ - "ScheduleTypeBackfill", - "ScheduleTypeNormal" - ] - }, - "github_com_raids-lab_crater_dao_model.Status": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "StatusActive": "Active status", - "StatusInactive": "Inactive status", - "StatusPending": "Pending status, not yet activated" - }, - "x-enum-descriptions": [ - "", - "Pending status, not yet activated", - "Active status", - "Inactive status" - ], - "x-enum-varnames": [ - "_", - "StatusPending", - "StatusActive", - "StatusInactive" - ] + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_service_ResourceLimitCheckResult": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult" + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.UserAttribute": { + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail": { "type": "object", "properties": { - "avatar": { - "description": "头像", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "email": { - "description": "邮箱", - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail" }, - "expiredAt": { - "description": "过期时间", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "gid": { - "description": "GID", - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUInfo" }, - "group": { - "description": "课题组", + "msg": { "type": "string" - }, - "id": { - "description": "ID", + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdjustUserExtraBalanceResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "name": { - "description": "账号", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.AdjustUserExtraBalanceResp" }, - "nickname": { - "description": "昵称,如果没有指定,则与账号相同", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdminModelDownloadLimitConfigResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "phone": { - "description": "电话", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.AdminModelDownloadLimitConfigResp" }, - "teacher": { - "description": "老师", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ApprovalOrderResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "uid": { - "description": "UID and GID are used for Filesystem", + "data": { + "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.UserBanAction": { - "type": "string", - "enum": [ - "ban", - "extend", - "unban" - ], - "x-enum-varnames": [ - "UserBanActionBan", - "UserBanActionExtend", - "UserBanActionUnban" - ] - }, - "github_com_raids-lab_crater_dao_model.UserBanRestrictions": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AuthModeResp": { "type": "object", "properties": { - "datasetDownload": { - "type": "boolean" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "imageBuild": { - "type": "boolean" + "data": { + "$ref": "#/definitions/internal_handler.AuthModeResp" }, - "jobSubmission": { - "type": "boolean" + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CLICompatibilityInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "modelDownload": { - "type": "boolean" + "data": { + "$ref": "#/definitions/internal_handler.CLICompatibilityInfo" }, - "platformAccess": { - "type": "boolean" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.UserInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CheckResp": { "type": "object", "properties": { - "nickname": { - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "username": { + "data": { + "$ref": "#/definitions/internal_handler.CheckResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.Order": { - "type": "string", - "enum": [ - "asc", - "desc" - ], - "x-enum-varnames": [ - "Asc", - "Desc" - ] - }, - "github_com_raids-lab_crater_internal_payload.ResourceDetail": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp": { "type": "object", "properties": { - "label": { - "description": "显示名称 (例如 \"NVIDIA V100\", \"CPU\", \"内存\")", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "type": { - "description": "资源类型 (gpu, vgpu, rdma, common)", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.CurrentUserBanStatusResp" }, - "usage": { - "description": "用量 (核时/卡时/GiB时)", - "type": "number" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.StatisticsResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_DeleteProjectResp": { "type": "object", "properties": { - "series": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.TimePointData" - } + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "totalUsage": { - "description": "TotalUsage Key: ResourceName (如 nvidia.com/v100)\nValue: 包含 Label 和 Type 的详细对象", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.ResourceDetail" - } + "data": { + "$ref": "#/definitions/internal_handler.DeleteProjectResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.TimePointData": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_GpuAnalysisStatusResp": { "type": "object", "properties": { - "timestamp": { - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "usage": { - "description": "Key: ResourceName, Value: Usage", - "type": "object", - "additionalProperties": { - "type": "number", - "format": "float64" - } + "data": { + "$ref": "#/definitions/internal_handler.GpuAnalysisStatusResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.FacetItem": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_JobResourceSummaryResp": { "type": "object", "properties": { - "count": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "value": { + "data": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.FacetResponse": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp": { "type": "object", "properties": { - "facets": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetItem" - } - } + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp": { "type": "object", "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler_operations.OperationLogResp" - } - }, - "total": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" + }, + "data": { + "$ref": "#/definitions/internal_handler.KthenaConversationTurnResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp": { "type": "object", "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler_vcjob.JobResp" - } - }, - "page": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "page_size": { - "type": "integer" + "data": { + "$ref": "#/definitions/internal_handler.KthenaInferenceStatusResp" }, - "total": { - "type": "integer" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-any": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp": { "type": "object", "properties": { "code": { "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "data": {}, + "data": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateResp" + }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_github_com_raids-lab_crater_pkg_crclient_Pod": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp": { "type": "object", "properties": { "code": { @@ -10893,17 +12737,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.Pod" - } + "$ref": "#/definitions/internal_handler.KthenaServiceResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_AccountResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LLMConfigResp": { "type": "object", "properties": { "code": { @@ -10911,17 +12752,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.AccountResp" - } + "$ref": "#/definitions/internal_handler.LLMConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ApprovalOrderResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LoginResp": { "type": "object", "properties": { "code": { @@ -10929,17 +12767,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.ApprovalOrderResp" - } + "$ref": "#/definitions/internal_handler.LoginResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_GpuAnalysisWithJobInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp": { "type": "object", "properties": { "code": { @@ -10947,17 +12782,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.GpuAnalysisWithJobInfo" - } + "$ref": "#/definitions/internal_handler.ModelDownloadLimitConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ModelDownloadResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp": { "type": "object", "properties": { "code": { @@ -10965,17 +12797,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.ModelDownloadResp" - } + "$ref": "#/definitions/internal_handler.ModelDownloadListResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_dao_model_GpuAnalysis": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp": { "type": "object", "properties": { "code": { @@ -10983,14 +12812,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.GpuAnalysis" + "$ref": "#/definitions/internal_handler.ModelDownloadResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark": { "type": "object", "properties": { "code": { @@ -10998,14 +12827,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.StatisticsResp" + "$ref": "#/definitions/internal_handler.NodeMark" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PodBandwidthConfigResp": { "type": "object", "properties": { "code": { @@ -11013,14 +12842,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetResponse" + "$ref": "#/definitions/internal_handler.PodBandwidthConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_List-internal_handler_operations_OperationLogResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueConfigResp": { "type": "object", "properties": { "code": { @@ -11028,14 +12857,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp" + "$ref": "#/definitions/internal_handler.PrequeueConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueFeatureStatusResp": { "type": "object", "properties": { "code": { @@ -11043,14 +12872,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/internal_handler.PrequeueFeatureStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_service_ResourceLimitCheckResult": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp": { "type": "object", "properties": { "code": { @@ -11058,14 +12887,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult" + "$ref": "#/definitions/internal_handler.ProjectCreateResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PutUserInProjectResp": { "type": "object", "properties": { "code": { @@ -11073,14 +12902,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail" + "$ref": "#/definitions/internal_handler.PutUserInProjectResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaConfigItemResp": { "type": "object", "properties": { "code": { @@ -11088,14 +12917,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUInfo" + "$ref": "#/definitions/internal_handler.QueueQuotaConfigItemResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdjustUserExtraBalanceResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaResp": { "type": "object", "properties": { "code": { @@ -11103,14 +12932,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AdjustUserExtraBalanceResp" + "$ref": "#/definitions/internal_handler.QueueQuotaResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdminModelDownloadLimitConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq": { "type": "object", "properties": { "code": { @@ -11118,14 +12947,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AdminModelDownloadLimitConfigResp" + "$ref": "#/definitions/internal_handler.TokenReq" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ApprovalOrderResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TriggerAllJobsAnalysisResponse": { "type": "object", "properties": { "code": { @@ -11133,14 +12962,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + "$ref": "#/definitions/internal_handler.TriggerAllJobsAnalysisResponse" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AuthModeResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserBanStatusResp": { "type": "object", "properties": { "code": { @@ -11148,14 +12977,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AuthModeResp" + "$ref": "#/definitions/internal_handler.UserBanStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CLICompatibilityInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp": { "type": "object", "properties": { "code": { @@ -11163,14 +12992,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CLICompatibilityInfo" + "$ref": "#/definitions/internal_handler.UserDetailResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CheckResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp": { "type": "object", "properties": { "code": { @@ -11178,14 +13007,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CheckResp" + "$ref": "#/definitions/internal_handler.VisibleUserBanStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress": { "type": "object", "properties": { "code": { @@ -11193,14 +13022,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CurrentUserBanStatusResp" + "$ref": "#/definitions/internal_handler_tool.PodIngress" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_DeleteProjectResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp": { "type": "object", "properties": { "code": { @@ -11208,14 +13037,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.DeleteProjectResp" + "$ref": "#/definitions/internal_handler_tool.PodIngressResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_GpuAnalysisStatusResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport": { "type": "object", "properties": { "code": { @@ -11223,14 +13052,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.GpuAnalysisStatusResp" + "$ref": "#/definitions/internal_handler_tool.PodNodeport" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_JobResourceSummaryResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp": { "type": "object", "properties": { "code": { @@ -11238,14 +13067,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryResp" + "$ref": "#/definitions/internal_handler_tool.PodNodeportResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LLMConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp": { "type": "object", "properties": { "code": { @@ -11253,14 +13082,14 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.LLMConfigResp" + "$ref": "#/definitions/internal_handler_vcjob.JupyterTokenResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LoginResp": { + "github_com_raids-lab_crater_internal_resputil.Response-map_string_string": { "type": "object", "properties": { "code": { @@ -11268,615 +13097,838 @@ const docTemplate = `{ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.LoginResp" + "$ref": "#/definitions/map_string_string" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-string": { "type": "object", "properties": { "code": { "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadLimitConfigResp" + "data": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult": { + "type": "object", + "properties": { + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitDetail" + } + }, + "enabled": { + "type": "boolean" + }, + "exceeded": { + "type": "boolean" + } + } + }, + "github_com_raids-lab_crater_internal_service.ResourceLimitDetail": { + "type": "object", + "properties": { + "exceeded": { + "type": "boolean" + }, + "limit": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "used": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_service_vcjob.ForwardType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "_", + "IngressType", + "NodePortType" + ] + }, + "github_com_raids-lab_crater_internal_util.VolumeMount": { + "type": "object", + "properties": { + "datasetID": { + "type": "integer" + }, + "mountPath": { + "type": "string" + }, + "subPath": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_util.VolumeType" + } + } + }, + "github_com_raids-lab_crater_internal_util.VolumeType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "_", + "FileType", + "DataType" + ] + }, + "github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "allocatable": { + "$ref": "#/definitions/v1.ResourceList" + }, + "arch": { + "type": "string" + }, + "capacity": { + "$ref": "#/definitions/v1.ResourceList" + }, + "containerRuntimeVersion": { + "type": "string" + }, + "gpuArch": { + "type": "string" + }, + "gpuCount": { + "type": "integer" + }, + "gpuDriver": { + "type": "string" + }, + "gpuMemory": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "kubeletVersion": { + "type": "string" + }, + "name": { + "type": "string" }, - "msg": { + "os": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadListResp" + "osVersion": { + "type": "string" }, - "msg": { + "role": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadResp" + "status": { + "$ref": "#/definitions/v1.NodeConditionType" }, - "msg": { + "taint": { + "type": "string" + }, + "time": { "type": "string" + }, + "used": { + "$ref": "#/definitions/v1.ResourceList" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark": { + "github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "arch": { + "description": "架构", + "type": "string" + }, + "count": { + "description": "数量", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.NodeMark" + "driver": { + "description": "驱动版本", + "type": "string" }, - "msg": { + "label": { + "description": "显示名称(从数据库获取,如 \"NVIDIA GPU\")", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PodBandwidthConfigResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PodBandwidthConfigResp" + "memory": { + "description": "显存", + "type": "string" }, - "msg": { + "product": { + "description": "具体型号(从节点标签获取,如 \"Tesla V100\",可选)", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueConfigResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PrequeueConfigResp" + "resourceName": { + "description": "资源名称,如 \"nvidia.com/gpu\"", + "type": "string" }, - "msg": { + "runtimeVersion": { + "description": "运行时版本(CUDA/ROCm 等)", + "type": "string" + }, + "vendorDomain": { + "description": "供应商域名,如 \"nvidia.com\"", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueFeatureStatusResp": { + "github_com_raids-lab_crater_pkg_crclient.GPUInfo": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "cudaVersion": { + "type": "string" + }, + "gpuArch": { + "type": "string" + }, + "gpuCount": { + "description": "总 GPU 数量", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PrequeueFeatureStatusResp" + "gpuDevices": { + "description": "多种类型的 GPU 设备列表", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo" + } }, - "msg": { + "gpuDriver": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ProjectCreateResp" + "gpuMemory": { + "description": "以下字段保留用于向后兼容(取第一个 GPU 设备的信息)", + "type": "string" }, - "msg": { + "gpuProduct": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PutUserInProjectResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PutUserInProjectResp" + "gpuUtil": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float32" + } }, - "msg": { + "haveGPU": { + "type": "boolean" + }, + "name": { "type": "string" + }, + "relateJobs": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaConfigItemResp": { + "github_com_raids-lab_crater_pkg_crclient.Pod": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "accountID": { + "description": "账户ID(用于跳转)", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.QueueQuotaConfigItemResp" + "accountName": { + "description": "账户昵称(用于显示)", + "type": "string" }, - "msg": { + "accountRealName": { + "description": "账户真实名称(用于tooltip)", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.QueueQuotaResp" + "createTime": { + "type": "string" }, - "msg": { + "ip": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.TokenReq" + "locked": { + "type": "boolean" }, - "msg": { + "lockedTimestamp": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TriggerAllJobsAnalysisResponse": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.TriggerAllJobsAnalysisResponse" + "name": { + "type": "string" }, - "msg": { + "namespace": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserBanStatusResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.UserBanStatusResp" + "ownerReference": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.OwnerReference" + } + }, + "permanentLocked": { + "type": "boolean" + }, + "requestResources": { + "description": "Requests", + "allOf": [ + { + "$ref": "#/definitions/v1.ResourceList" + } + ] + }, + "resources": { + "description": "Limits", + "allOf": [ + { + "$ref": "#/definitions/v1.ResourceList" + } + ] + }, + "status": { + "$ref": "#/definitions/v1.PodPhase" }, - "msg": { - "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "userID": { + "description": "用户ID(用于跳转)", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.UserDetailResp" + "userName": { + "description": "管理员接口返回的字段(omitempty 表示字段为空时不序列化)", + "type": "string" }, - "msg": { + "userRealName": { + "description": "用户真实名称(用于tooltip)", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp": { + "gorm.DeletedAt": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" - }, - "data": { - "$ref": "#/definitions/internal_handler.VisibleUserBanStatusResp" - }, - "msg": { + "time": { "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress": { + "internal_handler.AccountContext": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" + "accessPublic": { + "description": "User access mode of the platform", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" + } + ] }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodIngress" + "accessQueue": { + "description": "User access mode of the queue", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" + } + ] }, - "msg": { + "queue": { + "description": "Current Queue Name", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodIngressResp" + "rolePlatform": { + "description": "User role of the platform", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + } + ] }, - "msg": { + "roleQueue": { + "description": "User role of the queue", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + } + ] + }, + "space": { + "description": "User pvc subpath the platform", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport": { + "internal_handler.AccountResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" - }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodNodeport" + "access": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" }, - "msg": { + "expiredAt": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportResp" + "name": { + "type": "string" }, - "msg": { + "nickname": { "type": "string" + }, + "role": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp": { + "internal_handler.AdjustUserExtraBalanceReq": { "type": "object", + "required": [ + "delta" + ], "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "delta": { "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_vcjob.JupyterTokenResp" - }, - "msg": { + "reason": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-map_string_string": { + "internal_handler.AdjustUserExtraBalanceResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" + "afterBalance": { + "type": "number" }, - "data": { - "$ref": "#/definitions/map_string_string" + "beforeBalance": { + "type": "number" }, - "msg": { + "delta": { + "type": "number" + }, + "userId": { + "type": "integer" + }, + "username": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-string": { + "internal_handler.AdminModelDownloadLimitConfigResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "enabled": { + "type": "boolean" + }, + "maxConcurrent": { "type": "integer" }, - "data": { - "type": "string" + "maxSuccessfulDownloads": { + "type": "integer" }, - "msg": { - "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult": { - "type": "object", - "properties": { - "details": { + "whitelistUserIds": { "type": "array", "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitDetail" + "type": "integer" } }, - "enabled": { - "type": "boolean" - }, - "exceeded": { - "type": "boolean" + "windowHours": { + "type": "integer" } } }, - "github_com_raids-lab_crater_internal_service.ResourceLimitDetail": { + "internal_handler.ApprovalOrderResp": { "type": "object", "properties": { - "exceeded": { - "type": "boolean" + "content": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderContent" }, - "limit": { + "createdAt": { "type": "string" }, - "resource": { + "creator": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "creatorID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { "type": "string" }, - "used": { + "reviewNotes": { "type": "string" + }, + "reviewer": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "reviewerID": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" } } }, - "github_com_raids-lab_crater_internal_service_vcjob.ForwardType": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "_", - "IngressType", - "NodePortType" - ] - }, - "github_com_raids-lab_crater_internal_util.VolumeMount": { + "internal_handler.ApprovalOrderreq": { "type": "object", + "required": [ + "name", + "type" + ], "properties": { - "datasetID": { + "approvalOrderExtensionHours": { + "description": "延长小时数", "type": "integer" }, - "mountPath": { + "approvalOrderReason": { + "description": "审批原因", "type": "string" }, - "subPath": { + "approvalorderTypeID": { + "description": "关联的ID,可能是数据集或任务ID", + "type": "integer" + }, + "name": { "type": "string" }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + }, "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_util.VolumeType" + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" } } }, - "github_com_raids-lab_crater_internal_util.VolumeType": { - "type": "integer", + "internal_handler.AuthMethod": { + "type": "string", "enum": [ - 0, - 1, - 2 + "normal", + "ldap" ], "x-enum-varnames": [ - "_", - "FileType", - "DataType" + "AuthMethodNormal", + "AuthMethodLDAP" ] }, - "github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail": { + "internal_handler.AuthModeResp": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "allocatable": { - "$ref": "#/definitions/v1.ResourceList" + "enableLdap": { + "type": "boolean" }, - "arch": { - "type": "string" + "enableNormalLogin": { + "type": "boolean" }, - "capacity": { - "$ref": "#/definitions/v1.ResourceList" + "enableNormalRegister": { + "type": "boolean" }, - "containerRuntimeVersion": { + "ldapAlias": { "type": "string" }, - "gpuArch": { + "ldapHelp": { "type": "string" - }, - "gpuCount": { + } + } + }, + "internal_handler.CLICompatibilityInfo": { + "type": "object", + "properties": { + "apiVersion": { "type": "integer" }, - "gpuDriver": { - "type": "string" - }, - "gpuMemory": { + "appVersion": { "type": "string" }, - "kernelVersion": { + "buildTime": { "type": "string" }, - "kubeletVersion": { + "buildType": { "type": "string" }, - "name": { - "type": "string" + "minSupportedCliApiVersion": { + "type": "integer" }, - "os": { + "shortCommitSHA": { "type": "string" - }, - "osVersion": { + } + } + }, + "internal_handler.ChatMessage": { + "type": "object", + "properties": { + "content": { "type": "string" }, "role": { "type": "string" + } + } + }, + "internal_handler.CheckResp": { + "type": "object", + "properties": { + "context": { + "$ref": "#/definitions/internal_handler.AccountContext" }, - "status": { - "$ref": "#/definitions/v1.NodeConditionType" + "user": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute" }, - "taint": { + "version": { + "$ref": "#/definitions/internal_handler.VersionInfo" + } + } + }, + "internal_handler.CreateDownloadReq": { + "type": "object", + "required": [ + "category", + "name" + ], + "properties": { + "category": { + "type": "string", + "enum": [ + "model", + "dataset" + ] + }, + "name": { "type": "string" }, - "time": { + "revision": { "type": "string" }, - "used": { - "$ref": "#/definitions/v1.ResourceList" + "source": { + "type": "string" + }, + "token": { + "description": "Token is an optional access token for gated/private repositories on the\nsource site. It is only forwarded to the download Job as an env var and is\nnever persisted on the (shared, deduplicated) download record.", + "type": "string" } } }, - "github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo": { + "internal_handler.CreateKthenaReq": { "type": "object", + "required": [ + "name", + "worker" + ], "properties": { - "arch": { - "description": "架构", + "backendType": { "type": "string" }, - "count": { - "description": "数量", - "type": "integer" - }, - "driver": { - "description": "驱动版本", + "cacheURI": { "type": "string" }, - "label": { - "description": "显示名称(从数据库获取,如 \"NVIDIA GPU\")", + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "modelSource": { "type": "string" }, - "memory": { - "description": "显存", + "modelURI": { "type": "string" }, - "product": { - "description": "具体型号(从节点标签获取,如 \"Tesla V100\",可选)", + "name": { "type": "string" }, - "resourceName": { - "description": "资源名称,如 \"nvidia.com/gpu\"", + "platformModelId": { + "type": "integer" + }, + "replicas": { + "type": "integer" + }, + "selectors": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NodeSelectorRequirement" + } + }, + "servedModel": { "type": "string" }, - "runtimeVersion": { - "description": "运行时版本(CUDA/ROCm 等)", + "tolerations": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Toleration" + } + }, + "worker": { + "$ref": "#/definitions/internal_handler.KthenaWorkerReq" + } + } + }, + "internal_handler.CurrentUserBanStatusResp": { + "type": "object", + "properties": { + "banRestrictions": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserBanRestrictions" + }, + "banned": { + "type": "boolean" + }, + "bannedTimestamp": { "type": "string" }, - "vendorDomain": { - "description": "供应商域名,如 \"nvidia.com\"", + "permanentBanned": { + "type": "boolean" + }, + "reason": { "type": "string" } } }, - "github_com_raids-lab_crater_pkg_crclient.GPUInfo": { + "internal_handler.DatasetReq": { "type": "object", + "required": [ + "describe", + "name", + "url" + ], "properties": { - "cudaVersion": { + "describe": { "type": "string" }, - "gpuArch": { - "type": "string" + "editable": { + "type": "boolean" }, - "gpuCount": { - "description": "总 GPU 数量", - "type": "integer" + "ispublic": { + "type": "boolean" }, - "gpuDevices": { - "description": "多种类型的 GPU 设备列表", + "name": { + "type": "string" + }, + "tags": { "type": "array", "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo" + "type": "string" } }, - "gpuDriver": { - "type": "string" + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.DataType" }, - "gpuMemory": { - "description": "以下字段保留用于向后兼容(取第一个 GPU 设备的信息)", + "url": { "type": "string" }, - "gpuProduct": { + "weburl": { + "type": "string" + } + } + }, + "internal_handler.DeleteProjectResp": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "internal_handler.DownloadActionReq": { + "type": "object", + "properties": { + "revision": { + "description": "Revision is optional and only used by retry. A non-nil empty value means\n\"use the source default branch\" while preserving the failed record's path.", "type": "string" }, - "gpuUtil": { - "type": "object", - "additionalProperties": { - "type": "number", - "format": "float32" - } - }, - "haveGPU": { + "token": { + "type": "string" + } + } + }, + "internal_handler.FilePermission": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "_", + "NotAllowed", + "ReadOnly", + "ReadWrite" + ] + }, + "internal_handler.GpuAnalysisStatusResp": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" - }, - "name": { - "type": "string" - }, - "relateJobs": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } } } }, - "github_com_raids-lab_crater_pkg_crclient.Pod": { + "internal_handler.GpuAnalysisWithJobInfo": { "type": "object", "properties": { - "accountID": { - "description": "账户ID(用于跳转)", - "type": "integer" + "UserNickname": { + "type": "string" }, - "accountName": { - "description": "账户昵称(用于显示)", + "command": { + "description": "采集到的原始数据", "type": "string" }, - "accountRealName": { - "description": "账户真实名称(用于tooltip)", + "createdAt": { + "description": "自动追踪的时间戳", "type": "string" }, - "createTime": { + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "historicalMetrics": { "type": "string" }, - "ip": { + "id": { + "type": "integer" + }, + "jobID": { + "type": "integer" + }, + "jobName": { "type": "string" }, - "locked": { - "type": "boolean" + "jobType": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.JobType" + }, + "llmversion": { + "type": "string" }, "lockedTimestamp": { "type": "string" @@ -11887,486 +13939,494 @@ const docTemplate = `{ "namespace": { "type": "string" }, - "ownerReference": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.OwnerReference" - } + "nodes": { + "$ref": "#/definitions/datatypes.JSONType-array_string" }, - "permanentLocked": { - "type": "boolean" + "phase1LLMReason": { + "type": "string" }, - "requestResources": { - "description": "Requests", - "allOf": [ - { - "$ref": "#/definitions/v1.ResourceList" - } - ] + "phase1Score": { + "description": "LLM 分析结果", + "type": "integer" + }, + "phase2LLMReason": { + "type": "string" + }, + "phase2Score": { + "type": "integer" + }, + "podName": { + "description": "原始 Kubernetes 信息", + "type": "string" }, "resources": { - "description": "Limits", + "$ref": "#/definitions/datatypes.JSONType-v1_ResourceList" + }, + "reviewStatus": { + "description": "管理状态", "allOf": [ { - "$ref": "#/definitions/v1.ResourceList" + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" } ] }, "status": { - "$ref": "#/definitions/v1.PodPhase" + "$ref": "#/definitions/volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase" }, "userID": { - "description": "用户ID(用于跳转)", "type": "integer" }, "userName": { - "description": "管理员接口返回的字段(omitempty 表示字段为空时不序列化)", - "type": "string" - }, - "userRealName": { - "description": "用户真实名称(用于tooltip)", "type": "string" } } }, - "gorm.DeletedAt": { + "internal_handler.JobResourceSummaryAcceleratorResp": { "type": "object", "properties": { - "time": { + "limit": { "type": "string" }, - "valid": { - "description": "Valid is true if Time is not NULL", - "type": "boolean" + "pending": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "running": { + "type": "string" + }, + "used": { + "type": "string" } } }, - "internal_handler.AccountContext": { + "internal_handler.JobResourceSummaryResp": { "type": "object", "properties": { - "accessPublic": { - "description": "User access mode of the platform", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - } - ] - }, - "accessQueue": { - "description": "User access mode of the queue", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - } - ] + "accelerators": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryAcceleratorResp" + } }, - "queue": { - "description": "Current Queue Name", - "type": "string" + "cpu": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" }, - "rolePlatform": { - "description": "User role of the platform", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" - } - ] + "memory": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" }, - "roleQueue": { - "description": "User role of the queue", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" - } - ] + "pendingJobs": { + "type": "integer" }, - "space": { - "description": "User pvc subpath the platform", - "type": "string" + "runningJobs": { + "type": "integer" } } }, - "internal_handler.AccountResp": { + "internal_handler.JobResourceSummaryUsageResp": { "type": "object", "properties": { - "access": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - }, - "expiredAt": { + "limit": { "type": "string" }, - "name": { + "pending": { "type": "string" }, - "nickname": { + "running": { "type": "string" }, - "role": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + "used": { + "type": "string" } } }, - "internal_handler.AdjustUserExtraBalanceReq": { + "internal_handler.JobTemplateReq": { "type": "object", "required": [ - "delta" + "name", + "template" ], "properties": { - "delta": { - "type": "integer" + "describe": { + "type": "string" }, - "reason": { + "document": { + "type": "string" + }, + "name": { + "type": "string" + }, + "template": { "type": "string" } } }, - "internal_handler.AdjustUserExtraBalanceResp": { + "internal_handler.KthenaAccess": { "type": "object", "properties": { - "afterBalance": { - "type": "number" + "internalBaseURL": { + "type": "string" }, - "beforeBalance": { - "type": "number" + "modelName": { + "type": "string" }, - "delta": { - "type": "number" + "nodePortURL": { + "type": "string" }, - "userId": { - "type": "integer" + "proxyBaseURL": { + "type": "string" }, - "username": { + "routeName": { + "type": "string" + }, + "routerService": { + "type": "string" + }, + "serverName": { "type": "string" } } }, - "internal_handler.AdminModelDownloadLimitConfigResp": { + "internal_handler.KthenaConversationCreateReq": { "type": "object", "properties": { - "enabled": { - "type": "boolean" - }, - "maxConcurrent": { - "type": "integer" - }, - "maxSuccessfulDownloads": { - "type": "integer" - }, - "whitelistUserIds": { + "messages": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/internal_handler.KthenaConversationMessageReq" } }, - "windowHours": { + "sessionId": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "internal_handler.KthenaConversationMessageReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "internal_handler.KthenaConversationMessageResp": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "role": { + "type": "string" + }, + "sequence": { "type": "integer" } } }, - "internal_handler.ApprovalOrderResp": { + "internal_handler.KthenaConversationResp": { "type": "object", "properties": { - "content": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderContent" + "backendType": { + "type": "string" }, "createdAt": { "type": "string" }, - "creator": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" - }, - "creatorID": { + "messageCount": { "type": "integer" }, - "id": { - "type": "integer" + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageResp" + } }, - "name": { + "modelName": { "type": "string" }, - "reviewNotes": { + "namespace": { "type": "string" }, - "reviewer": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + "serviceName": { + "type": "string" }, - "reviewerID": { - "type": "integer" + "sessionId": { + "type": "string" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + "title": { + "type": "string" }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" + "updatedAt": { + "type": "string" } } }, - "internal_handler.ApprovalOrderreq": { + "internal_handler.KthenaConversationTurnReq": { "type": "object", - "required": [ - "name", - "type" - ], "properties": { - "approvalOrderExtensionHours": { - "description": "延长小时数", - "type": "integer" + "clientTurnId": { + "type": "string" }, - "approvalOrderReason": { - "description": "审批原因", + "content": { "type": "string" }, - "approvalorderTypeID": { - "description": "关联的ID,可能是数据集或任务ID", + "maxTokens": { "type": "integer" }, - "name": { + "sessionId": { "type": "string" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" - }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" + "temperature": { + "type": "number" } } }, - "internal_handler.AuthMethod": { - "type": "string", - "enum": [ - "normal", - "ldap" - ], - "x-enum-varnames": [ - "AuthMethodNormal", - "AuthMethodLDAP" - ] - }, - "internal_handler.AuthModeResp": { + "internal_handler.KthenaConversationTurnResp": { "type": "object", "properties": { - "enableLdap": { - "type": "boolean" - }, - "enableNormalLogin": { - "type": "boolean" + "assistant": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageResp" }, - "enableNormalRegister": { - "type": "boolean" + "completion": { + "type": "object" }, - "ldapAlias": { - "type": "string" + "conversation": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + } + } + }, + "internal_handler.KthenaConversationUpdateReq": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageReq" + } }, - "ldapHelp": { + "title": { "type": "string" } } }, - "internal_handler.CLICompatibilityInfo": { + "internal_handler.KthenaDiagnostic": { "type": "object", "properties": { - "apiVersion": { - "type": "integer" + "container": { + "type": "string" }, - "appVersion": { + "details": { "type": "string" }, - "buildTime": { + "level": { "type": "string" }, - "buildType": { + "message": { "type": "string" }, - "minSupportedCliApiVersion": { - "type": "integer" + "pod": { + "type": "string" }, - "shortCommitSHA": { + "reason": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "timestamp": { "type": "string" } } }, - "internal_handler.CheckResp": { + "internal_handler.KthenaInferenceStatusResp": { "type": "object", "properties": { - "context": { - "$ref": "#/definitions/internal_handler.AccountContext" - }, - "user": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute" - }, - "version": { - "$ref": "#/definitions/internal_handler.VersionInfo" + "enabled": { + "type": "boolean" } } }, - "internal_handler.CreateDownloadReq": { + "internal_handler.KthenaInferenceTemplateReq": { "type": "object", "required": [ - "category", + "config", "name" ], "properties": { - "category": { - "type": "string", - "enum": [ - "model", - "dataset" - ] + "config": { + "type": "object" + }, + "description": { + "type": "string" }, "name": { "type": "string" + } + } + }, + "internal_handler.KthenaInferenceTemplateResp": { + "type": "object", + "properties": { + "config": { + "type": "object" }, - "revision": { + "createdAt": { "type": "string" }, - "source": { + "description": { "type": "string" }, - "token": { - "description": "Token is an optional access token for gated/private repositories on the\nsource site. It is only forwarded to the download Job as an env var and is\nnever persisted on the (shared, deduplicated) download record.", + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { "type": "string" } } }, - "internal_handler.CurrentUserBanStatusResp": { + "internal_handler.KthenaProxyReq": { "type": "object", "properties": { - "banRestrictions": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserBanRestrictions" + "max_tokens": { + "type": "integer" }, - "banned": { - "type": "boolean" + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ChatMessage" + } }, - "bannedTimestamp": { + "model": { "type": "string" }, - "permanentBanned": { + "prompt": {}, + "stream": { "type": "boolean" }, - "reason": { - "type": "string" + "temperature": { + "type": "number" } } }, - "internal_handler.DatasetReq": { + "internal_handler.KthenaResource": { "type": "object", - "required": [ - "describe", - "name", - "url" - ], "properties": { - "describe": { - "type": "string" - }, - "editable": { - "type": "boolean" - }, - "ispublic": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "tags": { + "conditions": { "type": "array", "items": { - "type": "string" + "type": "object", + "additionalProperties": {} } }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.DataType" + "kind": { + "type": "string" }, - "url": { + "name": { "type": "string" }, - "weburl": { + "namespace": { + "type": "string" + }, + "phase": { "type": "string" + }, + "ready": { + "type": "boolean" } } }, - "internal_handler.DeleteProjectResp": { + "internal_handler.KthenaRuntimePod": { "type": "object", - "required": [ - "name" - ], "properties": { + "hostIP": { + "type": "string" + }, "name": { "type": "string" - } - } - }, - "internal_handler.DownloadActionReq": { - "type": "object", - "properties": { - "revision": { - "description": "Revision is optional and only used by retry. A non-nil empty value means\n\"use the source default branch\" while preserving the failed record's path.", + }, + "namespace": { "type": "string" }, - "token": { + "nodeName": { "type": "string" - } - } - }, - "internal_handler.FilePermission": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "_", - "NotAllowed", - "ReadOnly", - "ReadWrite" - ] - }, - "internal_handler.GpuAnalysisStatusResp": { - "type": "object", - "properties": { - "enabled": { + }, + "phase": { + "type": "string" + }, + "podIP": { + "type": "string" + }, + "ready": { "type": "boolean" + }, + "readyContainers": { + "type": "integer" + }, + "restarts": { + "type": "integer" + }, + "totalContainers": { + "type": "integer" } } }, - "internal_handler.GpuAnalysisWithJobInfo": { + "internal_handler.KthenaServiceResp": { "type": "object", "properties": { - "UserNickname": { - "type": "string" + "access": { + "$ref": "#/definitions/internal_handler.KthenaAccess" }, - "command": { - "description": "采集到的原始数据", + "backendType": { "type": "string" }, - "createdAt": { - "description": "自动追踪的时间戳", + "cacheURI": { "type": "string" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "conditions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } }, - "historicalMetrics": { + "createdAt": { "type": "string" }, - "id": { - "type": "integer" - }, - "jobID": { - "type": "integer" + "diagnostics": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaDiagnostic" + } }, - "jobName": { - "type": "string" + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "jobType": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.JobType" + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "llmversion": { + "modelSource": { "type": "string" }, - "lockedTimestamp": { + "modelURI": { "type": "string" }, "name": { @@ -12375,126 +14435,94 @@ const docTemplate = `{ "namespace": { "type": "string" }, - "nodes": { - "$ref": "#/definitions/datatypes.JSONType-array_string" - }, - "phase1LLMReason": { + "owner": { "type": "string" }, - "phase1Score": { - "description": "LLM 分析结果", - "type": "integer" - }, - "phase2LLMReason": { + "phase": { "type": "string" }, - "phase2Score": { + "platformModelId": { "type": "integer" }, - "podName": { - "description": "原始 Kubernetes 信息", - "type": "string" + "replicas": { + "type": "integer" }, "resources": { - "$ref": "#/definitions/datatypes.JSONType-v1_ResourceList" + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaResource" + } }, - "reviewStatus": { - "description": "管理状态", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" - } - ] + "runtimePods": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaRuntimePod" + } }, - "status": { - "$ref": "#/definitions/volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase" + "servedModel": { + "type": "string" }, - "userID": { - "type": "integer" + "userInfo": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" }, - "userName": { - "type": "string" - } - } - }, - "internal_handler.JobResourceSummaryAcceleratorResp": { - "type": "object", - "properties": { - "limit": { + "workerCPU": { "type": "string" }, - "pending": { + "workerConfig": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "workerGPU": { "type": "string" }, - "resource": { + "workerGPUModel": { "type": "string" }, - "running": { + "workerImage": { "type": "string" }, - "used": { + "workerMemory": { "type": "string" + }, + "workerReplicas": { + "type": "integer" } } }, - "internal_handler.JobResourceSummaryResp": { + "internal_handler.KthenaWorkerReq": { "type": "object", + "required": [ + "image" + ], "properties": { - "accelerators": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryAcceleratorResp" + "config": { + "type": "object", + "additionalProperties": { + "type": "string" } }, "cpu": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" - }, - "memory": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" - }, - "pendingJobs": { - "type": "integer" - }, - "runningJobs": { - "type": "integer" - } - } - }, - "internal_handler.JobResourceSummaryUsageResp": { - "type": "object", - "properties": { - "limit": { "type": "string" }, - "pending": { + "gpu": { "type": "string" }, - "running": { + "gpuModel": { "type": "string" }, - "used": { - "type": "string" - } - } - }, - "internal_handler.JobTemplateReq": { - "type": "object", - "required": [ - "name", - "template" - ], - "properties": { - "describe": { + "image": { "type": "string" }, - "document": { + "memory": { "type": "string" }, - "name": { - "type": "string" + "pods": { + "type": "integer" }, - "template": { - "type": "string" + "replicas": { + "type": "integer" } } }, @@ -12978,6 +15006,17 @@ const docTemplate = `{ } } }, + "internal_handler.SetKthenaInferenceStatusReq": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "internal_handler.SharedQueueReq": { "type": "object", "required": [ @@ -14122,6 +16161,89 @@ const docTemplate = `{ } } }, + "internal_handler_vcjob.WorkloadResp": { + "type": "object", + "properties": { + "billedPointsTotal": { + "type": "number" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "detailPath": { + "type": "string" + }, + "jobName": { + "type": "string" + }, + "jobType": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "lockedTimestamp": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "owner": { + "type": "string" + }, + "permanentLocked": { + "type": "boolean" + }, + "queue": { + "type": "string" + }, + "resources": { + "$ref": "#/definitions/v1.ResourceList" + }, + "scheduleType": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ScheduleType" + }, + "scheduler": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "status": { + "type": "string" + }, + "statusDetail": { + "type": "string" + }, + "userInfo": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "waitingToleranceSeconds": { + "type": "integer" + }, + "workloadID": { + "type": "string" + }, + "workloadKind": { + "type": "string" + } + } + }, "map_string_string": { "type": "object", "additionalProperties": { @@ -14394,6 +16516,63 @@ const docTemplate = `{ } } }, + "v1.TaintEffect": { + "type": "string", + "enum": [ + "NoSchedule", + "PreferNoSchedule", + "NoExecute" + ], + "x-enum-varnames": [ + "TaintEffectNoSchedule", + "TaintEffectPreferNoSchedule", + "TaintEffectNoExecute" + ] + }, + "v1.Toleration": { + "type": "object", + "properties": { + "effect": { + "description": "Effect indicates the taint effect to match. Empty means match all taint effects.\nWhen specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.\n+optional", + "allOf": [ + { + "$ref": "#/definitions/v1.TaintEffect" + } + ] + }, + "key": { + "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys.\nIf the key is empty, operator must be Exists; this combination means to match all values and all keys.\n+optional", + "type": "string" + }, + "operator": { + "description": "Operator represents a key's relationship to the value.\nValid operators are Exists and Equal. Defaults to Equal.\nExists is equivalent to wildcard for value, so that a pod can\ntolerate all taints of a particular category.\n+optional", + "allOf": [ + { + "$ref": "#/definitions/v1.TolerationOperator" + } + ] + }, + "tolerationSeconds": { + "description": "TolerationSeconds represents the period of time the toleration (which must be\nof effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,\nit is not set, which means tolerate the taint forever (do not evict). Zero and\nnegative values will be treated as 0 (evict immediately) by the system.\n+optional", + "type": "integer" + }, + "value": { + "description": "Value is the taint value the toleration matches to.\nIf the operator is Exists, the value should be empty, otherwise just a regular string.\n+optional", + "type": "string" + } + } + }, + "v1.TolerationOperator": { + "type": "string", + "enum": [ + "Exists", + "Equal" + ], + "x-enum-varnames": [ + "TolerationOpExists", + "TolerationOpEqual" + ] + }, "volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase": { "type": "string", "enum": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index b9f73ae7f..1b03e944d 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1756,6 +1756,185 @@ "responses": {} } }, + "/v1/admin/kthena/inference-services": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List all Kthena inference services managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "List all inference services", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/admin/kthena/inference-services/{name}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get any Kthena inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Get inference service as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete any Kthena inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Delete inference service as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/admin/kthena/inference-services/{name}/yaml": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get raw Kthena ModelBooster object for any inference service managed by Crater.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "Get inference service YAML as admin", + "parameters": [ + { + "type": "string", + "description": "Inference service name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, "/v1/admin/models/downloads": { "get": { "security": [ @@ -3289,6 +3468,74 @@ } } }, + "/v1/admin/system-config/kthena-inference": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。", + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "获取模型部署功能开关状态", + "responses": { + "200": { + "description": "开关状态", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp" + } + } + } + }, + "put": { + "security": [ + { + "Bearer": [] + } + ], + "description": "开启后,用户可以创建和管理基于 Kthena 的在线模型部署;关闭后所有模型部署接口均会拒绝访问。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "设置模型部署功能开关", + "parameters": [ + { + "description": "开关设置", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.SetKthenaInferenceStatusReq" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, "/v1/admin/system-config/llm": { "get": { "security": [ @@ -6670,14 +6917,14 @@ } } }, - "/v1/models/download": { - "post": { + "/v1/kthena/inference-services": { + "get": { "security": [ { "Bearer": [] } ], - "description": "创建一个新的模型下载任务", + "description": "List Kthena inference services owned by the current user and account.", "consumes": [ "application/json" ], @@ -6685,38 +6932,31 @@ "application/json" ], "tags": [ - "ModelDownload" - ], - "summary": "创建模型下载任务", - "parameters": [ - { - "description": "下载请求", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.CreateDownloadReq" - } - } + "kthena" ], + "summary": "List my inference services", "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/models/downloads": { - "get": { + }, + "post": { "security": [ { "Bearer": [] } ], - "description": "下载记录对全平台用户可见。带 page 参数时返回分页结构(含状态汇总),否则返回全量数组(兼容旧客户端)", + "description": "Create a Kthena ModelBooster-backed inference service.", "consumes": [ "application/json" ], @@ -6724,59 +6964,50 @@ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取模型下载任务列表", + "summary": "Create inference service", "parameters": [ { - "type": "string", - "description": "过滤类别: model 或 dataset", - "name": "category", - "in": "query" - }, - { - "type": "integer", - "description": "页码(从1开始);不传则返回全量数组", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "每页数量,默认20,最大100", - "name": "pageSize", - "in": "query" - }, - { - "type": "string", - "description": "过滤状态: Pending/Downloading/Paused/Ready/Failed", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "按名称模糊搜索", - "name": "search", - "in": "query" + "description": "Create inference service request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.CreateKthenaReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/models/downloads/{id}": { + "/v1/kthena/inference-services/{name}": { "get": { "security": [ { "Bearer": [] } ], - "description": "根据 ID 获取模型下载任务详情", + "description": "Get a Kthena inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -6784,14 +7015,14 @@ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取单个模型下载任务详情", + "summary": "Get inference service", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true } @@ -6800,7 +7031,19 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } @@ -6811,7 +7054,7 @@ "Bearer": [] } ], - "description": "删除下载任务记录(仅平台管理员),已下载的文件保留在存储中", + "description": "Delete a Kthena inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -6819,14 +7062,14 @@ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "删除模型下载任务", + "summary": "Delete inference service", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true } @@ -6837,55 +7080,92 @@ "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } } } } }, - "/v1/models/downloads/{id}/logs": { + "/v1/kthena/inference-services/{name}/conversations": { "get": { "security": [ { "Bearer": [] } ], - "description": "返回定时持久化到下载记录中的日志", - "consumes": [ - "application/json" - ], + "description": "List a user's deployment-scoped conversations; messages are omitted by default.", "produces": [ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "获取模型下载任务日志", + "summary": "List model deployment conversations", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true + }, + { + "type": "boolean", + "description": "Include recent messages", + "name": "includeMessages", + "in": "query" + }, + { + "type": "integer", + "description": "Conversation limit, maximum 100", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Messages per conversation, maximum 500", + "name": "messageLimit", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/models/downloads/{id}/pause": { + }, "post": { "security": [ { "Bearer": [] } ], - "description": "暂停正在进行的模型下载任务", + "description": "Create a user/deployment-scoped conversation; empty sessionId gets a UUID and supplied UUIDs are idempotent.", "consumes": [ "application/json" ], @@ -6893,36 +7173,63 @@ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "暂停下载任务", + "summary": "Create model deployment conversation", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true + }, + { + "description": "Conversation", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationCreateReq" + } } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/models/downloads/{id}/resume": { + "/v1/kthena/inference-services/{name}/conversations/turns": { "post": { "security": [ { "Bearer": [] } ], - "description": "恢复已暂停的模型下载任务", + "description": "Send an atomic turn without a path sessionId. Provide a body sessionId to reuse a UUID, or leave it empty for a new UUID.", "consumes": [ "application/json" ], @@ -6930,23 +7237,24 @@ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "恢复下载任务", + "summary": "Send a new or existing persisted model deployment conversation turn", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { - "description": "可选的临时访问令牌", - "name": "data", + "description": "User turn", + "name": "request", "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/internal_handler.DownloadActionReq" + "$ref": "#/definitions/internal_handler.KthenaConversationTurnReq" } } ], @@ -6954,121 +7262,150 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "502": { + "description": "Bad Gateway", + "schema": {} } } } }, - "/v1/models/downloads/{id}/retry": { - "post": { + "/v1/kthena/inference-services/{name}/conversations/{sessionId}": { + "get": { "security": [ { "Bearer": [] } ], - "description": "重新提交失败的模型下载任务", - "consumes": [ - "application/json" - ], + "description": "Get one persisted conversation and its most recent ordered messages for the current user and authorized Kthena deployment.", "produces": [ "application/json" ], "tags": [ - "ModelDownload" + "kthena" ], - "summary": "重试失败的下载任务", + "summary": "Get model deployment conversation", "parameters": [ { - "type": "integer", - "description": "下载任务ID", - "name": "id", + "type": "string", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { - "description": "可选的临时访问令牌和重试版本", - "name": "data", - "in": "body", - "schema": { - "$ref": "#/definitions/internal_handler.DownloadActionReq" - } + "type": "string", + "description": "Conversation session UUID", + "name": "sessionId", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Maximum recent messages, maximum 500", + "name": "messageLimit", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "获取Pod的容器列表", - "consumes": [ - "application/json" - ], + "description": "Permanently delete one current user's persisted Kthena conversation and all of its messages.", "produces": [ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的容器列表", + "summary": "Delete model deployment conversation", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "Pod名称", - "name": "name", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true } ], "responses": { "200": { - "description": "Pod容器列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, - "400": { - "description": "Request parameter error", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/log": { - "get": { + }, + "patch": { "security": [ { "Bearer": [] } ], - "description": "获取Pod容器日志", + "description": "Update title and/or replace all messages of a current user's conversation. Sending messages: [] clears the message list.", "consumes": [ "application/json" ], @@ -7076,96 +7413,55 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod容器日志", + "summary": "Update model deployment conversation", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", + "description": "Inference service name", "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "容器名称", - "name": "container", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true }, { - "type": "integer", - "description": "页码", - "name": "page", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "每页数量", - "name": "size", - "in": "query", - "required": true + "description": "Conversation update", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationUpdateReq" + } } ], "responses": { "200": { - "description": "Pod容器日志", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp" } }, "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - } - } - } - }, - "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/resources": { - "put": { - "description": "edit pod's resources(cpu, mem)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Operations" - ], - "summary": "edit pod's resources(cpu, mem)", - "responses": { - "200": { - "description": "Success", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "400": { - "description": "Request parameter error", + "404": { + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -7173,14 +7469,14 @@ } } }, - "/v1/namespaces/{namespace}/pods/{name}/events": { - "get": { + "/v1/kthena/inference-services/{name}/conversations/{sessionId}/turns": { + "post": { "security": [ { "Bearer": [] } ], - "description": "获取Pod的事件", + "description": "Use stored context to call Kthena and atomically save successful user and assistant messages.", "consumes": [ "application/json" ], @@ -7188,61 +7484,74 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的事件", + "summary": "Send a persisted model deployment conversation turn", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "任务名称", - "name": "name", + "description": "Conversation session UUID", + "name": "sessionId", "in": "path", "required": true + }, + { + "description": "User turn", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaConversationTurnReq" + } } ], "responses": { "200": { - "description": "Pod事件列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp" } }, "400": { - "description": "请求参数错误", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "任务未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + }, + "502": { + "description": "Bad Gateway", + "schema": {} } } } }, - "/v1/namespaces/{namespace}/pods/{name}/ingresses": { - "get": { + "/v1/kthena/inference-services/{name}/openai/{path}": { + "post": { "security": [ { "Bearer": [] } ], - "description": "通过Pod注解获取相关的Ingress规则", + "description": "Proxy an OpenAI-compatible request to kthena-router for an inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -7250,59 +7559,65 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的Ingress规则", + "summary": "Proxy OpenAI-compatible inference request", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", + "description": "Inference service name", + "name": "name", "in": "path", "required": true }, { "type": "string", - "description": "Pod名称", - "name": "name", + "description": "OpenAI-compatible API path", + "name": "path", "in": "path", "required": true + }, + { + "description": "OpenAI-compatible request body", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.KthenaProxyReq" + } } ], "responses": { "200": { - "description": "Pod Ingress规则列表", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp" - } + "description": "OK", + "schema": {} }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "Pod未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } + "description": "Model route or runtime pod not found", + "schema": {} }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "post": { + } + }, + "/v1/kthena/inference-services/{name}/yaml": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定Pod创建新的Ingress规则,规则名称和端口号必须唯一", + "description": "Get raw Kthena ModelBooster object for an inference service owned by the current user and account.", "consumes": [ "application/json" ], @@ -7310,68 +7625,77 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "创建新的Pod Ingress规则", + "summary": "Get inference service YAML", "parameters": [ { "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", + "description": "Inference service name", "name": "name", "in": "path", "required": true - }, - { - "description": "Ingress规则内容", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" - } } ], "responses": { "200": { - "description": "成功创建的Ingress规则", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误或规则冲突", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "404": { - "description": "Pod未找到", + "500": { + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } + } + } + } + }, + "/v1/kthena/inference-templates": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List the current user's templates in the active account. Templates are never shared across users or accounts.", + "produces": [ + "application/json" + ], + "tags": [ + "kthena" + ], + "summary": "List private Kthena deployment templates", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp" + } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "delete": { + "post": { "security": [ { "Bearer": [] } ], - "description": "根据规则名称删除指定的Ingress规则,同时删除关联的Service和Ingress", + "description": "Save the current deployment form as a private template for the current user and account.", "consumes": [ "application/json" ], @@ -7379,55 +7703,41 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "删除Pod的Ingress规则", + "summary": "Create a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "要删除的Ingress规则", - "name": "body", + "description": "Template", + "name": "request", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateReq" } } ], "responses": { "200": { - "description": "Ingress规则删除成功", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp" } }, "400": { - "description": "请求参数错误或Ingress规则未找到", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "404": { - "description": "Pod未找到", + "409": { + "description": "Conflict", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -7435,14 +7745,14 @@ } } }, - "/v1/namespaces/{namespace}/pods/{name}/nodeports": { - "get": { + "/v1/kthena/inference-templates/{id}": { + "put": { "security": [ { "Bearer": [] } ], - "description": "通过Pod的labels选择相关的Service并获取NodePort规则", + "description": "Replace a template owned by the current user in the active account.", "consumes": [ "application/json" ], @@ -7450,128 +7760,107 @@ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "获取Pod的NodePort规则", + "summary": "Update a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", + "type": "integer", + "description": "Template ID", + "name": "id", "in": "path", "required": true }, { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true + "description": "Template", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateReq" + } } ], "responses": { "200": { - "description": "Pod NodePort规则列表", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp" } }, "400": { - "description": "请求参数错误", + "description": "Bad Request", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "404": { - "description": "Pod未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "post": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "为指定Pod创建新的NodePort规则,规则名称和端口号必须唯一", - "consumes": [ - "application/json" - ], + "description": "Delete one template owned by the current user in the active account.", "produces": [ "application/json" ], "tags": [ - "Pod" + "kthena" ], - "summary": "创建新的Pod NodePort规则", + "summary": "Delete a private Kthena deployment template", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", + "type": "integer", + "description": "Template ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "NodePort规则内容", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" - } } ], "responses": { "200": { - "description": "成功创建的NodePort规则", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport" - } - }, - "400": { - "description": "请求参数错误或规则冲突", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "404": { - "description": "Pod未找到", + "description": "Not Found", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "delete": { + } + }, + "/v1/models/download": { + "post": { "security": [ { "Bearer": [] } ], - "description": "根据规则名称删除指定的NodePort规则,同时删除关联的Service", + "description": "创建一个新的模型下载任务", "consumes": [ "application/json" ], @@ -7579,70 +7868,38 @@ "application/json" ], "tags": [ - "Pod" + "ModelDownload" ], - "summary": "删除Pod的NodePort规则", + "summary": "创建模型下载任务", "parameters": [ { - "type": "string", - "description": "命名空间", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Pod名称", - "name": "name", - "in": "path", - "required": true - }, - { - "description": "要删除的NodePort规则", - "name": "body", + "description": "下载请求", + "name": "data", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" + "$ref": "#/definitions/internal_handler.CreateDownloadReq" } } ], "responses": { "200": { - "description": "NodePort规则删除成功", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误或NodePort规则未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "404": { - "description": "Pod未找到", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes": { + "/v1/models/downloads": { "get": { "security": [ { "Bearer": [] } ], - "description": "kubectl + prometheus获取节点基本信息", + "description": "下载记录对全平台用户可见。带 page 参数时返回分页结构(含状态汇总),否则返回全量数组(兼容旧客户端)", "consumes": [ "application/json" ], @@ -7650,39 +7907,59 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取节点基本信息", - "responses": { - "200": { - "description": "成功返回值描述,注意这里返回Json字符串,swagger无法准确解析", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } + "summary": "获取模型下载任务列表", + "parameters": [ + { + "type": "string", + "description": "过滤类别: model 或 dataset", + "name": "category", + "in": "query" }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } + { + "type": "integer", + "description": "页码(从1开始);不传则返回全量数组", + "name": "page", + "in": "query" }, - "500": { - "description": "其他错误", + { + "type": "integer", + "description": "每页数量,默认20,最大100", + "name": "pageSize", + "in": "query" + }, + { + "type": "string", + "description": "过滤状态: Pending/Downloading/Paused/Ready/Failed", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "按名称模糊搜索", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp" } } } } }, - "/v1/nodes/{name}": { + "/v1/models/downloads/{id}": { "get": { "security": [ { "Bearer": [] } ], - "description": "kubectl + prometheus获取节点详细信息", + "description": "根据 ID 获取模型下载任务详情", "consumes": [ "application/json" ], @@ -7690,46 +7967,34 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取节点详细信息", + "summary": "获取单个模型下载任务详情", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true } ], "responses": { "200": { - "description": "成功返回值", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } }, - "put": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "介绍函数的主要实现逻辑", + "description": "删除下载任务记录(仅平台管理员),已下载的文件保留在存储中", "consumes": [ "application/json" ], @@ -7737,57 +8002,36 @@ "application/json" ], "tags": [ - "接口对应的标签" + "ModelDownload" ], - "summary": "更新节点调度状态", + "summary": "删除模型下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "请求体,包含 reason 字段", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeScheduleRequest" - } } ], "responses": { "200": { - "description": "成功返回值", + "description": "OK", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } } } } }, - "/v1/nodes/{name}/annotation": { - "post": { + "/v1/models/downloads/{id}/logs": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加注解", + "description": "返回定时持久化到下载记录中的日志", "consumes": [ "application/json" ], @@ -7795,55 +8039,36 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "添加节点注解", + "summary": "获取模型下载任务日志", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "注解信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeAnnotation" - } } ], "responses": { "200": { - "description": "成功添加注解", + "description": "OK", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } } } - }, - "delete": { + } + }, + "/v1/models/downloads/{id}/pause": { + "post": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的注解", + "description": "暂停正在进行的模型下载任务", "consumes": [ "application/json" ], @@ -7851,57 +8076,36 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "删除节点注解", + "summary": "暂停下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true - }, - { - "description": "注解信息(只需要key)", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeAnnotation" - } } ], "responses": { "200": { - "description": "成功删除注解", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes/{name}/gpu/": { - "get": { + "/v1/models/downloads/{id}/resume": { + "post": { "security": [ { "Bearer": [] } ], - "description": "查询prometheus获取GPU各节点的利用率", + "description": "恢复已暂停的模型下载任务", "consumes": [ "application/json" ], @@ -7909,47 +8113,44 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "获取GPU各节点的利用率", + "summary": "恢复下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", - "in": "query" + "type": "integer", + "description": "下载任务ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "可选的临时访问令牌", + "name": "data", + "in": "body", + "schema": { + "$ref": "#/definitions/internal_handler.DownloadActionReq" + } } ], "responses": { "200": { - "description": "成功返回值描述", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } } }, - "/v1/nodes/{name}/label": { + "/v1/models/downloads/{id}/retry": { "post": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加标签", + "description": "重新提交失败的模型下载任务", "consumes": [ "application/json" ], @@ -7957,55 +8158,44 @@ "application/json" ], "tags": [ - "Node" + "ModelDownload" ], - "summary": "添加节点标签", + "summary": "重试失败的下载任务", "parameters": [ { - "type": "string", - "description": "节点名称", - "name": "name", + "type": "integer", + "description": "下载任务ID", + "name": "id", "in": "path", "required": true }, { - "description": "标签信息", + "description": "可选的临时访问令牌和重试版本", "name": "data", "in": "body", - "required": true, "schema": { - "$ref": "#/definitions/internal_handler.NodeLabel" + "$ref": "#/definitions/internal_handler.DownloadActionReq" } } ], "responses": { "200": { - "description": "成功添加标签", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" - } - }, - "400": { - "description": "请求参数错误", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "其他错误", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp" } } } - }, - "delete": { + } + }, + "/v1/namespaces/{namespace}/pods/{name}/containers": { + "get": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的标签", + "description": "获取Pod的容器列表", "consumes": [ "application/json" ], @@ -8013,42 +8203,40 @@ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "删除节点标签", + "summary": "获取Pod的容器列表", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "标签信息(只需要key)", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeLabel" - } + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功删除标签", + "description": "Pod容器列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8056,14 +8244,14 @@ } } }, - "/v1/nodes/{name}/mark": { + "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/log": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定节点的Labels、Annotations和Taints信息", + "description": "获取Pod容器日志", "consumes": [ "application/json" ], @@ -8071,33 +8259,61 @@ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "获取节点标记信息", + "summary": "获取Pod容器日志", "parameters": [ { "type": "string", - "description": "节点名称", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", "name": "name", "in": "path", "required": true + }, + { + "type": "string", + "description": "容器名称", + "name": "container", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "每页数量", + "name": "size", + "in": "query", + "required": true } ], "responses": { "200": { - "description": "成功返回节点标记信息", + "description": "Pod容器日志", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8105,14 +8321,9 @@ } } }, - "/v1/nodes/{name}/pod/": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "kubectl + prometheus获取节点Pod信息", + "/v1/namespaces/{namespace}/pods/{name}/containers/{container}/resources": { + "put": { + "description": "edit pod's resources(cpu, mem)", "consumes": [ "application/json" ], @@ -8120,32 +8331,24 @@ "application/json" ], "tags": [ - "Node" - ], - "summary": "获取节点Pod信息", - "parameters": [ - { - "type": "string", - "description": "节点名称", - "name": "name", - "in": "query" - } + "Operations" ], + "summary": "edit pod's resources(cpu, mem)", "responses": { "200": { - "description": "成功返回值描述", + "description": "Success", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "请求参数错误", + "description": "Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "其他错误", + "description": "Other errors", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8153,14 +8356,14 @@ } } }, - "/v1/nodes/{name}/taint": { - "post": { + "/v1/namespaces/{namespace}/pods/{name}/events": { + "get": { "security": [ { "Bearer": [] } ], - "description": "为指定节点添加污点", + "description": "获取Pod的事件", "consumes": [ "application/json" ], @@ -8168,32 +8371,30 @@ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "添加节点污点", + "summary": "获取Pod的事件", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "污点信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeTaint" - } + "type": "string", + "description": "任务名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功添加污点", + "description": "Pod事件列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { @@ -8202,21 +8403,29 @@ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "500": { - "description": "其他错误", + "404": { + "description": "任务未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - }, - "delete": { + } + }, + "/v1/namespaces/{namespace}/pods/{name}/ingresses": { + "get": { "security": [ { "Bearer": [] } ], - "description": "删除指定节点的污点", + "description": "通过Pod注解获取相关的Ingress规则", "consumes": [ "application/json" ], @@ -8224,32 +8433,30 @@ "application/json" ], "tags": [ - "Node" + "Pod" ], - "summary": "删除节点污点", + "summary": "获取Pod的Ingress规则", "parameters": [ { "type": "string", - "description": "节点名称", - "name": "name", + "description": "命名空间", + "name": "namespace", "in": "path", "required": true }, { - "description": "污点信息", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler.NodeTaint" - } + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功删除污点", + "description": "Pod Ingress规则列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp" } }, "400": { @@ -8258,6 +8465,12 @@ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, + "404": { + "description": "Pod未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, "500": { "description": "其他错误", "schema": { @@ -8265,11 +8478,14 @@ } } } - } - }, - "/v1/operations/add/locktime": { - "put": { - "description": "set LockTime of the job", + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "为指定Pod创建新的Ingress规则,规则名称和端口号必须唯一", "consumes": [ "application/json" ], @@ -8277,34 +8493,68 @@ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "创建新的Pod Ingress规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "Ingress规则内容", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + } + } ], - "summary": "set LockTime of the job", "responses": { "200": { - "description": "Success", + "description": "成功创建的Ingress规则", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或规则冲突", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/operations/clear/locktime": { - "put": { - "description": "clear LockTime of the job", + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据规则名称删除指定的Ingress规则,同时删除关联的Service和Ingress", "consumes": [ "application/json" ], @@ -8312,24 +8562,55 @@ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "删除Pod的Ingress规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "要删除的Ingress规则", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_tool.PodIngressMgr" + } + } ], - "summary": "clear LockTime of the job", "responses": { "200": { - "description": "Success", + "description": "Ingress规则删除成功", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或Ingress规则未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8337,14 +8618,14 @@ } } }, - "/v1/operations/cronjob": { + "/v1/namespaces/{namespace}/pods/{name}/nodeports": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get all cronjob configs", + "description": "通过Pod的labels选择相关的Service并获取NodePort规则", "consumes": [ "application/json" ], @@ -8352,37 +8633,59 @@ "application/json" ], "tags": [ - "Operations" + "Pod" + ], + "summary": "获取Pod的NodePort规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + } ], - "summary": "Get all cronjob configs", "responses": { "200": { - "description": "Success", + "description": "Pod NodePort规则列表", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } }, - "put": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Update one cronjob config", + "description": "为指定Pod创建新的NodePort规则,规则名称和端口号必须唯一", "consumes": [ "application/json" ], @@ -8390,45 +8693,68 @@ "application/json" ], "tags": [ - "Operations" + "Pod" ], - "summary": "Update cronjob config", + "summary": "创建新的Pod NodePort规则", "parameters": [ { - "description": "CronjobConfig", - "name": "use", + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "NodePort规则内容", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfig" + "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" } } ], "responses": { "200": { - "description": "Success", + "description": "成功创建的NodePort规则", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或规则冲突", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/operations/keep/{name}": { - "put": { - "description": "set KeepWhenLowResourceUsage of the job to the opposite value", + }, + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "根据规则名称删除指定的NodePort规则,同时删除关联的Service", "consumes": [ "application/json" ], @@ -8436,24 +8762,55 @@ "application/json" ], "tags": [ - "Operations" + "Pod" ], - "summary": "set KeepWhenLowResourceUsage of the job to the opposite value", - "responses": { - "200": { - "description": "Success", + "summary": "删除Pod的NodePort规则", + "parameters": [ + { + "type": "string", + "description": "命名空间", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Pod名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "要删除的NodePort规则", + "name": "body", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/internal_handler_tool.PodNodeportMgr" + } + } + ], + "responses": { + "200": { + "description": "NodePort规则删除成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误或NodePort规则未找到", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "Pod未找到", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8461,9 +8818,14 @@ } } }, - "/v1/operations/whitelist": { + "/v1/nodes": { "get": { - "description": "get job white list", + "security": [ + { + "Bearer": [] + } + ], + "description": "kubectl + prometheus获取节点基本信息", "consumes": [ "application/json" ], @@ -8471,24 +8833,24 @@ "application/json" ], "tags": [ - "Operations" + "Node" ], - "summary": "Get job white list", + "summary": "获取节点基本信息", "responses": { "200": { - "description": "Success", + "description": "成功返回值描述,注意这里返回Json字符串,swagger无法准确解析", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8496,14 +8858,14 @@ } } }, - "/v1/projects": { - "post": { + "/v1/nodes/{name}": { + "get": { "security": [ { "Bearer": [] } ], - "description": "从请求中获取账户名称、描述和配额,以当前用户为管理员,创建一个团队账户", + "description": "kubectl + prometheus获取节点详细信息", "consumes": [ "application/json" ], @@ -8511,23 +8873,23 @@ "application/json" ], "tags": [ - "Project" + "Node" ], - "summary": "创建团队账户", + "summary": "获取节点详细信息", "parameters": [ { - "description": "账户信息", - "name": "data", - "in": "body", - "required": true, - "schema": {} + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "成功创建账户,返回账户ID", + "description": "成功返回值", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail" } }, "400": { @@ -8537,22 +8899,20 @@ } }, "500": { - "description": "账户创建失败,返回错误信息", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/resources": { - "get": { + }, + "put": { "security": [ { "Bearer": [] } ], - "description": "If the vendorDomain parameter is provided, the API will return a list of resources that match the specified vendor domain.", + "description": "介绍函数的主要实现逻辑", "consumes": [ "application/json" ], @@ -8560,32 +8920,42 @@ "application/json" ], "tags": [ - "Resource" + "接口对应的标签" ], - "summary": "Get a list of resources based on the specified parameters", + "summary": "更新节点调度状态", "parameters": [ { "type": "string", - "description": "Vendor domain of the resource (For example: 'nvidia.com'\t)", - "name": "vendorDomain", - "in": "query" + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "请求体,包含 reason 字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeScheduleRequest" + } } ], "responses": { "200": { - "description": "Success", + "description": "成功返回值", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -8593,14 +8963,14 @@ } } }, - "/v1/resources/gpu/{gpuId}/networks": { - "get": { + "/v1/nodes/{name}/annotation": { + "post": { "security": [ { "Bearer": [] } ], - "description": "This API will return all RDMA resources linked to the specified GPU resource.", + "description": "为指定节点添加注解", "consumes": [ "application/json" ], @@ -8608,48 +8978,55 @@ "application/json" ], "tags": [ - "Resource" + "Node" ], - "summary": "Get all RDMA resources linked to a GPU resource", + "summary": "添加节点注解", "parameters": [ { - "type": "integer", - "description": "GPU Resource ID", - "name": "gpuId", + "type": "string", + "description": "节点名称", + "name": "name", "in": "path", "required": true + }, + { + "description": "注解信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeAnnotation" + } } ], "responses": { "200": { - "description": "Success", + "description": "成功添加注解", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/spjobs/{name}/events": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "获取稀疏推荐作业关联的事件信息", + "description": "删除指定节点的注解", "consumes": [ "application/json" ], @@ -8657,23 +9034,32 @@ "application/json" ], "tags": [ - "SpJob" + "Node" ], - "summary": "获取稀疏推荐作业的事件", + "summary": "删除节点注解", "parameters": [ { "type": "string", - "description": "Job Name", + "description": "节点名称", "name": "name", "in": "path", "required": true + }, + { + "description": "注解信息(只需要key)", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeAnnotation" + } } ], "responses": { "200": { - "description": "事件列表", + "description": "成功删除注解", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8691,14 +9077,14 @@ } } }, - "/v1/statistics": { + "/v1/nodes/{name}/gpu/": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定时间范围、指定维度的资源使用统计(核时/卡时)", + "description": "查询prometheus获取GPU各节点的利用率", "consumes": [ "application/json" ], @@ -8706,103 +9092,80 @@ "application/json" ], "tags": [ - "statistics" + "Node" ], - "summary": "获取资源统计信息", + "summary": "获取GPU各节点的利用率", "parameters": [ { "type": "string", - "description": "开始时间 (RFC3339)", - "name": "startTime", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "结束时间 (RFC3339)", - "name": "endTime", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "聚合粒度 (day/week)", - "name": "step", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "统计范围 (user/account/cluster)", - "name": "scope", - "in": "query", - "required": true - }, - { - "type": "integer", - "description": "目标ID (user_id 或 account_id)", - "name": "targetID", + "description": "节点名称", + "name": "name", "in": "query" } ], "responses": { "200": { - "description": "OK", + "description": "成功返回值描述", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/system-config/model-download-limit": { - "get": { + "/v1/nodes/{name}/label": { + "post": { "security": [ { "Bearer": [] } ], - "description": "获取当前用户的并发任务上限、滚动窗口成功下载上限和白名单豁免状态", + "description": "为指定节点添加标签", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "SystemConfig" + "Node" ], - "summary": "获取模型与数据集下载额度", - "responses": { - "200": { - "description": "OK", + "summary": "添加节点标签", + "parameters": [ + { + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "标签信息", + "name": "data", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp" + "$ref": "#/definitions/internal_handler.NodeLabel" } } - } - } - }, - "/v1/token/verify": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "读取header的auth进行鉴权", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" ], - "tags": [ - "Token" - ], - "summary": "通过token鉴权", "responses": { "200": { - "description": "Token 鉴权", + "description": "成功添加标签", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8818,56 +9181,47 @@ } } } - } - }, - "/v1/users/ban": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "返回当前用户仍在生效的封禁时间、原因和限制内容,不包含管理员操作历史", + "description": "删除指定节点的标签", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取当前用户封禁状态", - "responses": { - "200": { - "description": "OK", + "summary": "删除节点标签", + "parameters": [ + { + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "标签信息(只需要key)", + "name": "data", + "in": "body", + "required": true, "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp" + "$ref": "#/definitions/internal_handler.NodeLabel" } } - } - } - }, - "/v1/users/email/verified": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "检查邮箱是否已验证", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "User" ], - "summary": "检查邮箱是否已验证", "responses": { "200": { - "description": "成功获取用户信息", + "description": "成功删除标签", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { @@ -8885,14 +9239,14 @@ } } }, - "/v1/users/{name}": { + "/v1/nodes/{name}/mark": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取指定用户的详细信息", + "description": "获取指定节点的Labels、Annotations和Taints信息", "consumes": [ "application/json" ], @@ -8900,13 +9254,13 @@ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取单个用户信息", + "summary": "获取节点标记信息", "parameters": [ { "type": "string", - "description": "username", + "description": "节点名称", "name": "name", "in": "path", "required": true @@ -8914,9 +9268,9 @@ ], "responses": { "200": { - "description": "成功获取用户信息", + "description": "成功返回节点标记信息", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark" } }, "400": { @@ -8934,48 +9288,62 @@ } } }, - "/v1/users/{name}/ban": { + "/v1/nodes/{name}/pod/": { "get": { "security": [ { "Bearer": [] } ], - "description": "已登录用户可查看指定用户的封禁状态和记录,但不返回执行管理员信息", + "description": "kubectl + prometheus获取节点Pod信息", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "User" + "Node" ], - "summary": "获取用户封禁状态和记录", + "summary": "获取节点Pod信息", "parameters": [ { "type": "string", - "description": "username", + "description": "节点名称", "name": "name", - "in": "path", - "required": true + "in": "query" } ], "responses": { "200": { - "description": "OK", + "description": "成功返回值描述", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "其他错误", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs": { - "get": { + "/v1/nodes/{name}/taint": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Get the jobs of the user by client-go", + "description": "为指定节点添加污点", "consumes": [ "application/json" ], @@ -8983,107 +9351,55 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Node" ], - "summary": "Get the jobs of the user", + "summary": "添加节点污点", "parameters": [ - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, { "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" + "description": "节点名称", + "name": "name", + "in": "path", + "required": true }, { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" + "description": "污点信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeTaint" + } } ], "responses": { "200": { - "description": "Volcano Job List", + "description": "成功添加污点", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } - } - }, - "/v1/vcjobs/all": { - "get": { + }, + "delete": { "security": [ { "Bearer": [] } ], - "description": "返回指定天数内的所有作业,默认为14天", + "description": "删除指定节点的污点", "consumes": [ "application/json" ], @@ -9091,93 +9407,42 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Node" ], - "summary": "Get all of the jobs", + "summary": "删除节点污点", "parameters": [ { - "type": "integer", - "default": 14, - "description": "Number of days to look back, default is 14", - "name": "days", - "in": "query" + "type": "string", + "description": "节点名称", + "name": "name", + "in": "path", + "required": true }, { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" + "description": "污点信息", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler.NodeTaint" + } } ], "responses": { "200": { - "description": "admin get Volcano Job List", + "description": "成功删除污点", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string" } }, "400": { - "description": "admin Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9185,162 +9450,44 @@ } } }, - "/v1/vcjobs/all/facets": { - "get": { - "security": [ - { - "Bearer": [] - } + "/v1/operations/add/locktime": { + "put": { + "description": "set LockTime of the job", + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get all visible job facets", - "parameters": [ - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "set LockTime of the job", "responses": { "200": { - "description": "OK", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } - } - } - } - }, - "/v1/vcjobs/facets": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "produces": [ - "application/json" - ], - "tags": [ - "VolcanoJob" - ], - "summary": "Get job facets for the current user", - "parameters": [ - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", + "500": { + "description": "Other errors", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs/jupyter": { - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Create a Jupyter job", + "/v1/operations/clear/locktime": { + "put": { + "description": "clear LockTime of the job", "consumes": [ "application/json" ], @@ -9348,20 +9495,9 @@ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a Jupyter job", - "parameters": [ - { - "description": "Create Jupyter Job Request", - "name": "CreateJupyterReq", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" - } - } + "Operations" ], + "summary": "clear LockTime of the job", "responses": { "200": { "description": "Success", @@ -9384,14 +9520,14 @@ } } }, - "/v1/vcjobs/tensorflow": { - "post": { + "/v1/operations/cronjob": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Create a training job", + "description": "Get all cronjob configs", "consumes": [ "application/json" ], @@ -9399,18 +9535,9 @@ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a training job", - "parameters": [ - { - "description": "CreateTrainingReq", - "name": "CreateTrainingReq", - "in": "body", - "required": true, - "schema": {} - } + "Operations" ], + "summary": "Get all cronjob configs", "responses": { "200": { "description": "Success", @@ -9431,16 +9558,14 @@ } } } - } - }, - "/v1/vcjobs/training": { - "post": { + }, + "put": { "security": [ { "Bearer": [] } ], - "description": "Create a training job", + "description": "Update one cronjob config", "consumes": [ "application/json" ], @@ -9448,16 +9573,18 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Operations" ], - "summary": "Create a training job", + "summary": "Update cronjob config", "parameters": [ { - "description": "CreateTrainingReq", - "name": "CreateTrainingReq", + "description": "CronjobConfig", + "name": "use", "in": "body", "required": true, - "schema": {} + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfig" + } } ], "responses": { @@ -9482,14 +9609,9 @@ } } }, - "/v1/vcjobs/user/{username}": { - "get": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Get job list of a specific user within specified days. Both users and administrators can call this API.", + "/v1/operations/keep/{name}": { + "put": { + "description": "set KeepWhenLowResourceUsage of the job to the opposite value", "consumes": [ "application/json" ], @@ -9497,90 +9619,14 @@ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get jobs of a specific user within days", - "parameters": [ - { - "type": "string", - "description": "Username", - "name": "username", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 30, - "description": "Number of days to look back, default is 30, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "integer", - "description": "Page number", - "name": "page", - "in": "query" - }, - { - "type": "integer", - "description": "Page size, 1-200", - "name": "page_size", - "in": "query" - }, - { - "type": "string", - "description": "Sort fields", - "name": "sort", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "set KeepWhenLowResourceUsage of the job to the opposite value", "responses": { "200": { - "description": "User's Job List", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { @@ -9589,18 +9635,6 @@ "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, - "403": { - "description": "Forbidden - insufficient permissions", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "404": { - "description": "User not found", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, "500": { "description": "Other errors", "schema": { @@ -9610,95 +9644,49 @@ } } }, - "/v1/vcjobs/user/{username}/facets": { + "/v1/operations/whitelist": { "get": { - "security": [ - { - "Bearer": [] - } + "description": "get job white list", + "consumes": [ + "application/json" ], "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get job facets for a user", - "parameters": [ - { - "type": "string", - "description": "Username", - "name": "username", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Number of days to look back, -1 for all", - "name": "days", - "in": "query" - }, - { - "type": "string", - "description": "Search jobs", - "name": "search", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job types", - "name": "job_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "integer" - }, - "collectionFormat": "multi", - "description": "Schedule types", - "name": "schedule_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "description": "Job statuses", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Node name", - "name": "node", - "in": "query" - } + "Operations" ], + "summary": "Get job white list", "responses": { "200": { - "description": "OK", + "description": "Success", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } } } } }, - "/v1/vcjobs/webide": { + "/v1/projects": { "post": { "security": [ { "Bearer": [] } ], - "description": "Create a WebIDE job", + "description": "从请求中获取账户名称、描述和配额,以当前用户为管理员,创建一个团队账户", "consumes": [ "application/json" ], @@ -9706,35 +9694,33 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Project" ], - "summary": "Create a WebIDE job", + "summary": "创建团队账户", "parameters": [ { - "description": "Create WebIDE Job Request", - "name": "CreateJupyterReq", + "description": "账户信息", + "name": "data", "in": "body", "required": true, - "schema": { - "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" - } + "schema": {} } ], "responses": { "200": { - "description": "Success", + "description": "成功创建账户,返回账户ID", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "账户创建失败,返回错误信息", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9742,14 +9728,14 @@ } } }, - "/v1/vcjobs/{name}": { - "delete": { + "/v1/resources": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Delete the job by client-go", + "description": "If the vendorDomain parameter is provided, the API will return a list of resources that match the specified vendor domain.", "consumes": [ "application/json" ], @@ -9757,16 +9743,15 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Resource" ], - "summary": "Delete the job", + "summary": "Get a list of resources based on the specified parameters", "parameters": [ { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "description": "Vendor domain of the resource (For example: 'nvidia.com'\t)", + "name": "vendorDomain", + "in": "query" } ], "responses": { @@ -9791,9 +9776,14 @@ } } }, - "/v1/vcjobs/{name}/alert": { - "put": { - "description": "set AlertEnabled of the job to the opposite value", + "/v1/resources/gpu/{gpuId}/networks": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "This API will return all RDMA resources linked to the specified GPU resource.", "consumes": [ "application/json" ], @@ -9801,9 +9791,18 @@ "application/json" ], "tags": [ - "VolcanoJob" + "Resource" + ], + "summary": "Get all RDMA resources linked to a GPU resource", + "parameters": [ + { + "type": "integer", + "description": "GPU Resource ID", + "name": "gpuId", + "in": "path", + "required": true + } ], - "summary": "set AlertEnabled of the job to the opposite value", "responses": { "200": { "description": "Success", @@ -9826,14 +9825,14 @@ } } }, - "/v1/vcjobs/{name}/detail": { + "/v1/spjobs/{name}/events": { "get": { "security": [ { "Bearer": [] } ], - "description": "调用k8s get crd", + "description": "获取稀疏推荐作业关联的事件信息", "consumes": [ "application/json" ], @@ -9841,9 +9840,9 @@ "application/json" ], "tags": [ - "VolcanoJob" + "SpJob" ], - "summary": "获取jupyter详情", + "summary": "获取稀疏推荐作业的事件", "parameters": [ { "type": "string", @@ -9855,19 +9854,19 @@ ], "responses": { "200": { - "description": "任务描述", + "description": "事件列表", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -9875,14 +9874,14 @@ } } }, - "/v1/vcjobs/{name}/event": { + "/v1/statistics": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的事件", + "description": "获取指定时间范围、指定维度的资源使用统计(核时/卡时)", "consumes": [ "application/json" ], @@ -9890,97 +9889,113 @@ "application/json" ], "tags": [ - "VolcanoJob" + "statistics" ], - "summary": "获取任务的事件", + "summary": "获取资源统计信息", "parameters": [ { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", + "description": "开始时间 (RFC3339)", + "name": "startTime", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "结束时间 (RFC3339)", + "name": "endTime", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "聚合粒度 (day/week)", + "name": "step", + "in": "query", "required": true + }, + { + "type": "string", + "description": "统计范围 (user/account/cluster)", + "name": "scope", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "目标ID (user_id 或 account_id)", + "name": "targetID", + "in": "query" } ], "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp" } } } } }, - "/v1/vcjobs/{name}/pods": { + "/v1/system-config/kthena-inference": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的Pod列表", - "consumes": [ - "application/json" - ], + "description": "查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。", "produces": [ "application/json" ], "tags": [ - "VolcanoJob" + "SystemConfig" ], - "summary": "获取任务的Pod列表", - "parameters": [ + "summary": "获取模型部署功能开关状态", + "responses": { + "200": { + "description": "开关状态", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp" + } + } + } + } + }, + "/v1/system-config/model-download-limit": { + "get": { + "security": [ { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "Bearer": [] } ], + "description": "获取当前用户的并发任务上限、滚动窗口成功下载上限和白名单豁免状态", + "produces": [ + "application/json" + ], + "tags": [ + "SystemConfig" + ], + "summary": "获取模型与数据集下载额度", "responses": { "200": { - "description": "Pod列表", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp" } } } } }, - "/v1/vcjobs/{name}/secret": { + "/v1/token/verify": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get the password of the WebIDE job by reading config file in the pod", + "description": "读取header的auth进行鉴权", "consumes": [ "application/json" ], @@ -9988,33 +10003,24 @@ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Get the password of the WebIDE job", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "Token" ], + "summary": "通过token鉴权", "responses": { "200": { - "description": "Success", + "description": "Token 鉴权", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10022,63 +10028,39 @@ } } }, - "/v1/vcjobs/{name}/snapshot": { - "post": { + "/v1/users/ban": { + "get": { "security": [ { "Bearer": [] } ], - "description": "Create nerdctl docker commit to snapshot the job container (supports Jupyter and Custom job types)", - "consumes": [ - "application/json" - ], + "description": "返回当前用户仍在生效的封禁时间、原因和限制内容,不包含管理员操作历史", "produces": [ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "Create a snapshot of the job container", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "User" ], + "summary": "获取当前用户封禁状态", "responses": { "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" - } - }, - "400": { - "description": "Request parameter error", - "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" - } - }, - "500": { - "description": "Other errors", + "description": "OK", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp" } } } } }, - "/v1/vcjobs/{name}/ssh": { - "post": { + "/v1/users/email/verified": { + "get": { "security": [ { "Bearer": [] } ], - "description": "开启 SSH", + "description": "检查邮箱是否已验证", "consumes": [ "application/json" ], @@ -10086,33 +10068,24 @@ "application/json" ], "tags": [ - "VolcanoJob" - ], - "summary": "开启 SSH", - "parameters": [ - { - "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true - } + "User" ], + "summary": "检查邮箱是否已验证", "responses": { "200": { - "description": "SSH开启成功", + "description": "成功获取用户信息", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10120,14 +10093,14 @@ } } }, - "/v1/vcjobs/{name}/template": { + "/v1/users/{name}": { "get": { "security": [ { "Bearer": [] } ], - "description": "获取任务的 template", + "description": "获取指定用户的详细信息", "consumes": [ "application/json" ], @@ -10135,13 +10108,13 @@ "application/json" ], "tags": [ - "VolcanoJob" + "User" ], - "summary": "获取任务的 template", + "summary": "获取单个用户信息", "parameters": [ { "type": "string", - "description": "Job Name", + "description": "username", "name": "name", "in": "path", "required": true @@ -10149,19 +10122,19 @@ ], "responses": { "200": { - "description": "Success", + "description": "成功获取用户信息", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp" } }, "400": { - "description": "Request parameter error", + "description": "请求参数错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } }, "500": { - "description": "Other errors", + "description": "其他错误", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10169,14 +10142,48 @@ } } }, - "/v1/vcjobs/{name}/token": { + "/v1/users/{name}/ban": { "get": { "security": [ { "Bearer": [] } ], - "description": "Get the token of the job by logs", + "description": "已登录用户可查看指定用户的封禁状态和记录,但不返回执行管理员信息", + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "获取用户封禁状态和记录", + "parameters": [ + { + "type": "string", + "description": "username", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp" + } + } + } + } + }, + "/v1/vcjobs": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the jobs of the user by client-go", "consumes": [ "application/json" ], @@ -10186,21 +10193,80 @@ "tags": [ "VolcanoJob" ], - "summary": "Get the ingress base url and jupyter token of the job", + "summary": "Get the jobs of the user", "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, { "type": "string", - "description": "Job Name", - "name": "jobName", - "in": "path", - "required": true + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" } ], "responses": { "200": { - "description": "Success", + "description": "Volcano Job List", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" } }, "400": { @@ -10218,14 +10284,14 @@ } } }, - "/v1/vcjobs/{name}/yaml": { + "/v1/vcjobs/all": { "get": { "security": [ { "Bearer": [] } ], - "description": "调用k8s get crd", + "description": "返回指定天数内的所有作业,默认为14天", "consumes": [ "application/json" ], @@ -10233,27 +10299,87 @@ "application/json" ], "tags": [ - "vcjob-jupyter" + "VolcanoJob" ], - "summary": "获取vcjob Yaml详情", + "summary": "Get all of the jobs", "parameters": [ + { + "type": "integer", + "default": 14, + "description": "Number of days to look back, default is 14", + "name": "days", + "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, { "type": "string", - "description": "Job Name", - "name": "name", - "in": "path", - "required": true + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" } ], "responses": { "200": { - "description": "任务yaml", + "description": "admin get Volcano Job List", "schema": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" } }, "400": { - "description": "Request parameter error", + "description": "admin Request parameter error", "schema": { "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" } @@ -10266,618 +10392,2336 @@ } } } - } - }, - "definitions": { - "datatypes.JSONType-array_string": { - "type": "object" - }, - "datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota": { - "type": "object" - }, - "datatypes.JSONType-v1_ResourceList": { - "type": "object" - }, - "github_com_raids-lab_crater_dao_model.AccessMode": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3, - 4 - ], - "x-enum-comments": { - "AccessModeAO": "Append-only mode", - "AccessModeNA": "Not-allowed mode", - "AccessModeRO": "Read-only mode", - "AccessModeRW": "Read-write mode" - }, - "x-enum-descriptions": [ - "", - "Not-allowed mode", - "Read-only mode", - "Read-write mode", - "Append-only mode" - ], - "x-enum-varnames": [ - "_", - "AccessModeNA", - "AccessModeRO", - "AccessModeRW", - "AccessModeAO" - ] - }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderContent": { - "type": "object", - "properties": { - "approvalorderExtensionHours": { - "description": "延长小时数", - "type": "integer" - }, - "approvalorderReason": { - "description": "审批原因", - "type": "string" - }, - "approvalorderTypeID": { - "type": "integer" - } - } }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderStatus": { - "type": "string", - "enum": [ + "/v1/vcjobs/all/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get all visible job facets", + "parameters": [ + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get job facets for the current user", + "parameters": [ + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/jupyter": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a Jupyter job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a Jupyter job", + "parameters": [ + { + "description": "Create Jupyter Job Request", + "name": "CreateJupyterReq", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/tensorflow": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a training job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a training job", + "parameters": [ + { + "description": "CreateTrainingReq", + "name": "CreateTrainingReq", + "in": "body", + "required": true, + "schema": {} + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/training": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a training job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a training job", + "parameters": [ + { + "description": "CreateTrainingReq", + "name": "CreateTrainingReq", + "in": "body", + "required": true, + "schema": {} + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/user/{username}": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get job list of a specific user within specified days. Both users and administrators can call this API.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get jobs of a specific user within days", + "parameters": [ + { + "type": "string", + "description": "Username", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 30, + "description": "Number of days to look back, default is 30, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "User's Job List", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "404": { + "description": "User not found", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/user/{username}/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get job facets for a user", + "parameters": [ + { + "type": "string", + "description": "Username", + "name": "username", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "string", + "description": "Search jobs", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/webide": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a WebIDE job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a WebIDE job", + "parameters": [ + { + "description": "Create WebIDE Job Request", + "name": "CreateJupyterReq", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/internal_handler_vcjob.CreateJupyterReq" + } + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/workloads": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Lists persisted Volcano jobs and current-user Kthena ModelBoosters as a single pageable list.", + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get current user's unified workloads", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size, 1-200", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields", + "name": "sort", + "in": "query" + }, + { + "type": "string", + "description": "Search workloads", + "name": "search", + "in": "query" + }, + { + "type": "integer", + "description": "Number of days to look back, -1 for all", + "name": "days", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Job types, including model-deployment", + "name": "job_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Workload kinds", + "name": "workload_kind", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "multi", + "description": "Schedule types", + "name": "schedule_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "Workload statuses", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Node name", + "name": "node", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp" + } + } + } + } + }, + "/v1/vcjobs/workloads/facets": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get current user's unified workload facets", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse" + } + } + } + } + }, + "/v1/vcjobs/{name}": { + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete the job by client-go", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Delete the job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/alert": { + "put": { + "description": "set AlertEnabled of the job to the opposite value", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "set AlertEnabled of the job to the opposite value", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/detail": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "调用k8s get crd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取jupyter详情", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务描述", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/event": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的事件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的事件", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/pods": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的Pod列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的Pod列表", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pod列表", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/secret": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the password of the WebIDE job by reading config file in the pod", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get the password of the WebIDE job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/snapshot": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create nerdctl docker commit to snapshot the job container (supports Jupyter and Custom job types)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Create a snapshot of the job container", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/ssh": { + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "开启 SSH", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "开启 SSH", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "SSH开启成功", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/template": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "获取任务的 template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "获取任务的 template", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/token": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Get the token of the job by logs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "VolcanoJob" + ], + "summary": "Get the ingress base url and jupyter token of the job", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "jobName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + }, + "/v1/vcjobs/{name}/yaml": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "调用k8s get crd", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "vcjob-jupyter" + ], + "summary": "获取vcjob Yaml详情", + "parameters": [ + { + "type": "string", + "description": "Job Name", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "任务yaml", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "400": { + "description": "Request parameter error", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + }, + "500": { + "description": "Other errors", + "schema": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any" + } + } + } + } + } + }, + "definitions": { + "datatypes.JSONType-array_string": { + "type": "object" + }, + "datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota": { + "type": "object" + }, + "datatypes.JSONType-v1_ResourceList": { + "type": "object" + }, + "github_com_raids-lab_crater_dao_model.AccessMode": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ], + "x-enum-comments": { + "AccessModeAO": "Append-only mode", + "AccessModeNA": "Not-allowed mode", + "AccessModeRO": "Read-only mode", + "AccessModeRW": "Read-write mode" + }, + "x-enum-descriptions": [ + "", + "Not-allowed mode", + "Read-only mode", + "Read-write mode", + "Append-only mode" + ], + "x-enum-varnames": [ + "_", + "AccessModeNA", + "AccessModeRO", + "AccessModeRW", + "AccessModeAO" + ] + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderContent": { + "type": "object", + "properties": { + "approvalorderExtensionHours": { + "description": "延长小时数", + "type": "integer" + }, + "approvalorderReason": { + "description": "审批原因", + "type": "string" + }, + "approvalorderTypeID": { + "type": "integer" + } + } + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderStatus": { + "type": "string", + "enum": [ "Pending", "Approved", "Rejected", "Canceled" ], "x-enum-comments": { - "ApprovalOrderStatusApproved": "已批准", - "ApprovalOrderStatusCancelled": "已取消", - "ApprovalOrderStatusPending": "待审批", - "ApprovalOrderStatusRejected": "已拒绝" + "ApprovalOrderStatusApproved": "已批准", + "ApprovalOrderStatusCancelled": "已取消", + "ApprovalOrderStatusPending": "待审批", + "ApprovalOrderStatusRejected": "已拒绝" + }, + "x-enum-descriptions": [ + "待审批", + "已批准", + "已拒绝", + "已取消" + ], + "x-enum-varnames": [ + "ApprovalOrderStatusPending", + "ApprovalOrderStatusApproved", + "ApprovalOrderStatusRejected", + "ApprovalOrderStatusCancelled" + ] + }, + "github_com_raids-lab_crater_dao_model.ApprovalOrderType": { + "type": "string", + "enum": [ + "dataset", + "job" + ], + "x-enum-comments": { + "ApprovalOrderTypeDataset": "数据集类型", + "ApprovalOrderTypeJob": "任务类型" + }, + "x-enum-descriptions": [ + "数据集类型", + "任务类型" + ], + "x-enum-varnames": [ + "ApprovalOrderTypeDataset", + "ApprovalOrderTypeJob" + ] + }, + "github_com_raids-lab_crater_dao_model.BuildSource": { + "type": "string", + "enum": [ + "Dockerfile", + "PipApt", + "Snapshot", + "EnvdAdvanced", + "EnvdRaw" + ], + "x-enum-varnames": [ + "Dockerfile", + "PipApt", + "Snapshot", + "EnvdAdvanced", + "EnvdRaw" + ] + }, + "github_com_raids-lab_crater_dao_model.CraterResourceType": { + "type": "string", + "enum": [ + "gpu", + "rdma", + "vgpu" + ], + "x-enum-varnames": [ + "ResourceTypeGPU", + "ResourceTypeRDMA", + "ResourceTypeVGPU" + ] + }, + "github_com_raids-lab_crater_dao_model.CronJobConfig": { + "type": "object", + "properties": { + "config": { + "type": "array", + "items": { + "type": "integer" + } + }, + "createdAt": { + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "entry_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "spec": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfigStatus" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobType" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.CronJobConfigStatus": { + "type": "string", + "enum": [ + "unknown", + "suspended", + "idle", + "running" + ], + "x-enum-varnames": [ + "CronJobConfigStatusUnknown", + "CronJobConfigStatusSuspended", + "CronJobConfigStatusIdle", + "CronJobConfigStatusRunning" + ] + }, + "github_com_raids-lab_crater_dao_model.CronJobType": { + "type": "string", + "enum": [ + "cleaner_function", + "patrol_function" + ], + "x-enum-varnames": [ + "CronJobTypeCleanerFunc", + "CronJobTypePatrolFunc" + ] + }, + "github_com_raids-lab_crater_dao_model.DataType": { + "type": "string", + "enum": [ + "dataset", + "model", + "sharefile" + ], + "x-enum-varnames": [ + "DataTypeDataset", + "DataTypeModel", + "DataTypeShareFile" + ] + }, + "github_com_raids-lab_crater_dao_model.GpuAnalysis": { + "type": "object", + "properties": { + "command": { + "description": "采集到的原始数据", + "type": "string" + }, + "createdAt": { + "description": "自动追踪的时间戳", + "type": "string" + }, + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "historicalMetrics": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "jobID": { + "type": "integer" + }, + "jobName": { + "type": "string" + }, + "llmversion": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "phase1LLMReason": { + "type": "string" + }, + "phase1Score": { + "description": "LLM 分析结果", + "type": "integer" + }, + "phase2LLMReason": { + "type": "string" + }, + "phase2Score": { + "type": "integer" + }, + "podName": { + "description": "原始 Kubernetes 信息", + "type": "string" + }, + "reviewStatus": { + "description": "管理状态", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" + } + ] + }, + "userID": { + "type": "integer" + }, + "userName": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.JobType": { + "type": "string", + "enum": [ + "all", + "jupyter", + "webide", + "pytorch", + "tensorflow", + "kuberay", + "deepspeed", + "openmpi", + "custom" + ], + "x-enum-varnames": [ + "JobTypeAll", + "JobTypeJupyter", + "JobTypeWebIDE", + "JobTypePytorch", + "JobTypeTensorflow", + "JobTypeKubeRay", + "JobTypeDeepSpeed", + "JobTypeOpenMPI", + "JobTypeCustom" + ] + }, + "github_com_raids-lab_crater_dao_model.ReviewStatus": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "ReviewStatusConfirmed": "已确认", + "ReviewStatusIgnored": "已忽略", + "ReviewStatusPending": "待审核", + "_": "零值,被忽略" }, "x-enum-descriptions": [ - "待审批", - "已批准", - "已拒绝", - "已取消" + "零值,被忽略", + "待审核", + "已确认", + "已忽略" ], "x-enum-varnames": [ - "ApprovalOrderStatusPending", - "ApprovalOrderStatusApproved", - "ApprovalOrderStatusRejected", - "ApprovalOrderStatusCancelled" + "_", + "ReviewStatusPending", + "ReviewStatusConfirmed", + "ReviewStatusIgnored" + ] + }, + "github_com_raids-lab_crater_dao_model.Role": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "_", + "RoleGuest", + "RoleUser", + "RoleAdmin" + ] + }, + "github_com_raids-lab_crater_dao_model.ScheduleType": { + "type": "integer", + "enum": [ + 0, + 1 + ], + "x-enum-varnames": [ + "ScheduleTypeBackfill", + "ScheduleTypeNormal" + ] + }, + "github_com_raids-lab_crater_dao_model.Status": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "StatusActive": "Active status", + "StatusInactive": "Inactive status", + "StatusPending": "Pending status, not yet activated" + }, + "x-enum-descriptions": [ + "", + "Pending status, not yet activated", + "Active status", + "Inactive status" + ], + "x-enum-varnames": [ + "_", + "StatusPending", + "StatusActive", + "StatusInactive" + ] + }, + "github_com_raids-lab_crater_dao_model.UserAttribute": { + "type": "object", + "properties": { + "avatar": { + "description": "头像", + "type": "string" + }, + "email": { + "description": "邮箱", + "type": "string" + }, + "expiredAt": { + "description": "过期时间", + "type": "string" + }, + "gid": { + "description": "GID", + "type": "string" + }, + "group": { + "description": "课题组", + "type": "string" + }, + "id": { + "description": "ID", + "type": "integer" + }, + "name": { + "description": "账号", + "type": "string" + }, + "nickname": { + "description": "昵称,如果没有指定,则与账号相同", + "type": "string" + }, + "phone": { + "description": "电话", + "type": "string" + }, + "teacher": { + "description": "老师", + "type": "string" + }, + "uid": { + "description": "UID and GID are used for Filesystem", + "type": "string" + } + } + }, + "github_com_raids-lab_crater_dao_model.UserBanAction": { + "type": "string", + "enum": [ + "ban", + "extend", + "unban" + ], + "x-enum-varnames": [ + "UserBanActionBan", + "UserBanActionExtend", + "UserBanActionUnban" + ] + }, + "github_com_raids-lab_crater_dao_model.UserBanRestrictions": { + "type": "object", + "properties": { + "datasetDownload": { + "type": "boolean" + }, + "imageBuild": { + "type": "boolean" + }, + "jobSubmission": { + "type": "boolean" + }, + "modelDownload": { + "type": "boolean" + }, + "platformAccess": { + "type": "boolean" + } + } + }, + "github_com_raids-lab_crater_dao_model.UserInfo": { + "type": "object", + "properties": { + "nickname": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_payload.Order": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "x-enum-varnames": [ + "Asc", + "Desc" ] }, - "github_com_raids-lab_crater_dao_model.ApprovalOrderType": { - "type": "string", - "enum": [ - "dataset", - "job" - ], - "x-enum-comments": { - "ApprovalOrderTypeDataset": "数据集类型", - "ApprovalOrderTypeJob": "任务类型" - }, - "x-enum-descriptions": [ - "数据集类型", - "任务类型" - ], - "x-enum-varnames": [ - "ApprovalOrderTypeDataset", - "ApprovalOrderTypeJob" - ] + "github_com_raids-lab_crater_internal_payload.ResourceDetail": { + "type": "object", + "properties": { + "label": { + "description": "显示名称 (例如 \"NVIDIA V100\", \"CPU\", \"内存\")", + "type": "string" + }, + "type": { + "description": "资源类型 (gpu, vgpu, rdma, common)", + "type": "string" + }, + "usage": { + "description": "用量 (核时/卡时/GiB时)", + "type": "number" + } + } + }, + "github_com_raids-lab_crater_internal_payload.StatisticsResp": { + "type": "object", + "properties": { + "series": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.TimePointData" + } + }, + "totalUsage": { + "description": "TotalUsage Key: ResourceName (如 nvidia.com/v100)\nValue: 包含 Label 和 Type 的详细对象", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.ResourceDetail" + } + } + } + }, + "github_com_raids-lab_crater_internal_payload.TimePointData": { + "type": "object", + "properties": { + "timestamp": { + "type": "string" + }, + "usage": { + "description": "Key: ResourceName, Value: Usage", + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float64" + } + } + } + }, + "github_com_raids-lab_crater_internal_resputil.FacetItem": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "value": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.BuildSource": { - "type": "string", - "enum": [ - "Dockerfile", - "PipApt", - "Snapshot", - "EnvdAdvanced", - "EnvdRaw" - ], - "x-enum-varnames": [ - "Dockerfile", - "PipApt", - "Snapshot", - "EnvdAdvanced", - "EnvdRaw" - ] + "github_com_raids-lab_crater_internal_resputil.FacetResponse": { + "type": "object", + "properties": { + "facets": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetItem" + } + } + } + } }, - "github_com_raids-lab_crater_dao_model.CraterResourceType": { - "type": "string", - "enum": [ - "gpu", - "rdma", - "vgpu" - ], - "x-enum-varnames": [ - "ResourceTypeGPU", - "ResourceTypeRDMA", - "ResourceTypeVGPU" - ] + "github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler_operations.OperationLogResp" + } + }, + "total": { + "type": "integer" + } + } }, - "github_com_raids-lab_crater_dao_model.CronJobConfig": { + "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp": { "type": "object", "properties": { - "config": { + "items": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/internal_handler_vcjob.JobResp" } }, - "createdAt": { - "type": "string" + "page": { + "type": "integer" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "page_size": { + "type": "integer" }, - "entry_id": { + "total": { "type": "integer" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler_vcjob.WorkloadResp" + } }, - "id": { + "page": { "type": "integer" }, - "name": { + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-any": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": {}, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_github_com_raids-lab_crater_pkg_crclient_Pod": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "spec": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.Pod" + } + }, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_AccountResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobConfigStatus" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.AccountResp" + } }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.CronJobType" + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ApprovalOrderResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "updatedAt": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + } + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.CronJobConfigStatus": { - "type": "string", - "enum": [ - "unknown", - "suspended", - "idle", - "running" - ], - "x-enum-varnames": [ - "CronJobConfigStatusUnknown", - "CronJobConfigStatusSuspended", - "CronJobConfigStatusIdle", - "CronJobConfigStatusRunning" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_GpuAnalysisWithJobInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.GpuAnalysisWithJobInfo" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.CronJobType": { - "type": "string", - "enum": [ - "cleaner_function", - "patrol_function" - ], - "x-enum-varnames": [ - "CronJobTypeCleanerFunc", - "CronJobTypePatrolFunc" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.DataType": { - "type": "string", - "enum": [ - "dataset", - "model", - "sharefile" - ], - "x-enum-varnames": [ - "DataTypeDataset", - "DataTypeModel", - "DataTypeShareFile" - ] + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateResp" + } + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.GpuAnalysis": { + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp": { "type": "object", "properties": { - "command": { - "description": "采集到的原始数据", - "type": "string" - }, - "createdAt": { - "description": "自动追踪的时间戳", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaServiceResp" + } }, - "historicalMetrics": { + "msg": { "type": "string" - }, - "id": { + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ModelDownloadResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "jobID": { - "type": "integer" + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ModelDownloadResp" + } }, - "jobName": { + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_dao_model_GpuAnalysis": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "llmversion": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.GpuAnalysis" }, - "namespace": { + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "phase1LLMReason": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.StatisticsResp" }, - "phase1Score": { - "description": "LLM 分析结果", + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "phase2LLMReason": { - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetResponse" }, - "phase2Score": { + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_List-internal_handler_operations_OperationLogResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "podName": { - "description": "原始 Kubernetes 信息", + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp" + }, + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "reviewStatus": { - "description": "管理状态", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" - } - ] + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp" }, - "userID": { + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "userName": { + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.JobType": { - "type": "string", - "enum": [ - "all", - "jupyter", - "webide", - "pytorch", - "tensorflow", - "kuberay", - "deepspeed", - "openmpi", - "custom" - ], - "x-enum-varnames": [ - "JobTypeAll", - "JobTypeJupyter", - "JobTypeWebIDE", - "JobTypePytorch", - "JobTypeTensorflow", - "JobTypeKubeRay", - "JobTypeDeepSpeed", - "JobTypeOpenMPI", - "JobTypeCustom" - ] - }, - "github_com_raids-lab_crater_dao_model.ReviewStatus": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "ReviewStatusConfirmed": "已确认", - "ReviewStatusIgnored": "已忽略", - "ReviewStatusPending": "待审核", - "_": "零值,被忽略" - }, - "x-enum-descriptions": [ - "零值,被忽略", - "待审核", - "已确认", - "已忽略" - ], - "x-enum-varnames": [ - "_", - "ReviewStatusPending", - "ReviewStatusConfirmed", - "ReviewStatusIgnored" - ] - }, - "github_com_raids-lab_crater_dao_model.Role": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "_", - "RoleGuest", - "RoleUser", - "RoleAdmin" - ] - }, - "github_com_raids-lab_crater_dao_model.ScheduleType": { - "type": "integer", - "enum": [ - 0, - 1 - ], - "x-enum-varnames": [ - "ScheduleTypeBackfill", - "ScheduleTypeNormal" - ] - }, - "github_com_raids-lab_crater_dao_model.Status": { - "type": "integer", - "format": "int32", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-comments": { - "StatusActive": "Active status", - "StatusInactive": "Inactive status", - "StatusPending": "Pending status, not yet activated" - }, - "x-enum-descriptions": [ - "", - "Pending status, not yet activated", - "Active status", - "Inactive status" - ], - "x-enum-varnames": [ - "_", - "StatusPending", - "StatusActive", - "StatusInactive" - ] + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_service_ResourceLimitCheckResult": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult" + }, + "msg": { + "type": "string" + } + } }, - "github_com_raids-lab_crater_dao_model.UserAttribute": { + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail": { "type": "object", "properties": { - "avatar": { - "description": "头像", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "email": { - "description": "邮箱", - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail" }, - "expiredAt": { - "description": "过期时间", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "gid": { - "description": "GID", - "type": "string" + "data": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUInfo" }, - "group": { - "description": "课题组", + "msg": { "type": "string" - }, - "id": { - "description": "ID", + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdjustUserExtraBalanceResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "name": { - "description": "账号", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.AdjustUserExtraBalanceResp" }, - "nickname": { - "description": "昵称,如果没有指定,则与账号相同", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdminModelDownloadLimitConfigResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "phone": { - "description": "电话", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.AdminModelDownloadLimitConfigResp" }, - "teacher": { - "description": "老师", + "msg": { "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ApprovalOrderResp": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "uid": { - "description": "UID and GID are used for Filesystem", + "data": { + "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.UserBanAction": { - "type": "string", - "enum": [ - "ban", - "extend", - "unban" - ], - "x-enum-varnames": [ - "UserBanActionBan", - "UserBanActionExtend", - "UserBanActionUnban" - ] - }, - "github_com_raids-lab_crater_dao_model.UserBanRestrictions": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AuthModeResp": { "type": "object", "properties": { - "datasetDownload": { - "type": "boolean" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "imageBuild": { - "type": "boolean" + "data": { + "$ref": "#/definitions/internal_handler.AuthModeResp" }, - "jobSubmission": { - "type": "boolean" + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CLICompatibilityInfo": { + "type": "object", + "properties": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "modelDownload": { - "type": "boolean" + "data": { + "$ref": "#/definitions/internal_handler.CLICompatibilityInfo" }, - "platformAccess": { - "type": "boolean" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_dao_model.UserInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CheckResp": { "type": "object", "properties": { - "nickname": { - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "username": { + "data": { + "$ref": "#/definitions/internal_handler.CheckResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.Order": { - "type": "string", - "enum": [ - "asc", - "desc" - ], - "x-enum-varnames": [ - "Asc", - "Desc" - ] - }, - "github_com_raids-lab_crater_internal_payload.ResourceDetail": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp": { "type": "object", "properties": { - "label": { - "description": "显示名称 (例如 \"NVIDIA V100\", \"CPU\", \"内存\")", - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "type": { - "description": "资源类型 (gpu, vgpu, rdma, common)", - "type": "string" + "data": { + "$ref": "#/definitions/internal_handler.CurrentUserBanStatusResp" }, - "usage": { - "description": "用量 (核时/卡时/GiB时)", - "type": "number" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.StatisticsResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_DeleteProjectResp": { "type": "object", "properties": { - "series": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.TimePointData" - } + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "totalUsage": { - "description": "TotalUsage Key: ResourceName (如 nvidia.com/v100)\nValue: 包含 Label 和 Type 的详细对象", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.ResourceDetail" - } + "data": { + "$ref": "#/definitions/internal_handler.DeleteProjectResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_payload.TimePointData": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_GpuAnalysisStatusResp": { "type": "object", "properties": { - "timestamp": { - "type": "string" + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" }, - "usage": { - "description": "Key: ResourceName, Value: Usage", - "type": "object", - "additionalProperties": { - "type": "number", - "format": "float64" - } + "data": { + "$ref": "#/definitions/internal_handler.GpuAnalysisStatusResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.FacetItem": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_JobResourceSummaryResp": { "type": "object", "properties": { - "count": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "value": { + "data": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryResp" + }, + "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.FacetResponse": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp": { "type": "object", "properties": { - "facets": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetItem" - } - } + "code": { + "description": "依然保持 int (ErrorCode) 类型", + "type": "integer" + }, + "data": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp": { "type": "object", "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler_operations.OperationLogResp" - } - }, - "total": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" + }, + "data": { + "$ref": "#/definitions/internal_handler.KthenaConversationTurnResp" + }, + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp": { "type": "object", "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler_vcjob.JobResp" - } - }, - "page": { + "code": { + "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "page_size": { - "type": "integer" + "data": { + "$ref": "#/definitions/internal_handler.KthenaInferenceStatusResp" }, - "total": { - "type": "integer" + "msg": { + "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-any": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp": { "type": "object", "properties": { "code": { "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "data": {}, + "data": { + "$ref": "#/definitions/internal_handler.KthenaInferenceTemplateResp" + }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_github_com_raids-lab_crater_pkg_crclient_Pod": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp": { "type": "object", "properties": { "code": { @@ -10885,17 +12729,14 @@ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.Pod" - } + "$ref": "#/definitions/internal_handler.KthenaServiceResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_AccountResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LLMConfigResp": { "type": "object", "properties": { "code": { @@ -10903,17 +12744,14 @@ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.AccountResp" - } + "$ref": "#/definitions/internal_handler.LLMConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ApprovalOrderResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LoginResp": { "type": "object", "properties": { "code": { @@ -10921,17 +12759,14 @@ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.ApprovalOrderResp" - } + "$ref": "#/definitions/internal_handler.LoginResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_GpuAnalysisWithJobInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp": { "type": "object", "properties": { "code": { @@ -10939,17 +12774,14 @@ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.GpuAnalysisWithJobInfo" - } + "$ref": "#/definitions/internal_handler.ModelDownloadLimitConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ModelDownloadResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp": { "type": "object", "properties": { "code": { @@ -10957,17 +12789,14 @@ "type": "integer" }, "data": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.ModelDownloadResp" - } + "$ref": "#/definitions/internal_handler.ModelDownloadListResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_dao_model_GpuAnalysis": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp": { "type": "object", "properties": { "code": { @@ -10975,14 +12804,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.GpuAnalysis" + "$ref": "#/definitions/internal_handler.ModelDownloadResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_payload_StatisticsResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark": { "type": "object", "properties": { "code": { @@ -10990,14 +12819,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_payload.StatisticsResp" + "$ref": "#/definitions/internal_handler.NodeMark" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PodBandwidthConfigResp": { "type": "object", "properties": { "code": { @@ -11005,14 +12834,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.FacetResponse" + "$ref": "#/definitions/internal_handler.PodBandwidthConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_List-internal_handler_operations_OperationLogResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueConfigResp": { "type": "object", "properties": { "code": { @@ -11020,14 +12849,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.List-internal_handler_operations_OperationLogResp" + "$ref": "#/definitions/internal_handler.PrequeueConfigResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_JobResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueFeatureStatusResp": { "type": "object", "properties": { "code": { @@ -11035,14 +12864,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_JobResp" + "$ref": "#/definitions/internal_handler.PrequeueFeatureStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_service_ResourceLimitCheckResult": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp": { "type": "object", "properties": { "code": { @@ -11050,14 +12879,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult" + "$ref": "#/definitions/internal_handler.ProjectCreateResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_ClusterNodeDetail": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PutUserInProjectResp": { "type": "object", "properties": { "code": { @@ -11065,14 +12894,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail" + "$ref": "#/definitions/internal_handler.PutUserInProjectResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_pkg_crclient_GPUInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaConfigItemResp": { "type": "object", "properties": { "code": { @@ -11080,14 +12909,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUInfo" + "$ref": "#/definitions/internal_handler.QueueQuotaConfigItemResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdjustUserExtraBalanceResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaResp": { "type": "object", "properties": { "code": { @@ -11095,14 +12924,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AdjustUserExtraBalanceResp" + "$ref": "#/definitions/internal_handler.QueueQuotaResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AdminModelDownloadLimitConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq": { "type": "object", "properties": { "code": { @@ -11110,14 +12939,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AdminModelDownloadLimitConfigResp" + "$ref": "#/definitions/internal_handler.TokenReq" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ApprovalOrderResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TriggerAllJobsAnalysisResponse": { "type": "object", "properties": { "code": { @@ -11125,14 +12954,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.ApprovalOrderResp" + "$ref": "#/definitions/internal_handler.TriggerAllJobsAnalysisResponse" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_AuthModeResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserBanStatusResp": { "type": "object", "properties": { "code": { @@ -11140,14 +12969,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.AuthModeResp" + "$ref": "#/definitions/internal_handler.UserBanStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CLICompatibilityInfo": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp": { "type": "object", "properties": { "code": { @@ -11155,14 +12984,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CLICompatibilityInfo" + "$ref": "#/definitions/internal_handler.UserDetailResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CheckResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp": { "type": "object", "properties": { "code": { @@ -11170,14 +12999,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CheckResp" + "$ref": "#/definitions/internal_handler.VisibleUserBanStatusResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_CurrentUserBanStatusResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress": { "type": "object", "properties": { "code": { @@ -11185,14 +13014,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.CurrentUserBanStatusResp" + "$ref": "#/definitions/internal_handler_tool.PodIngress" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_DeleteProjectResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp": { "type": "object", "properties": { "code": { @@ -11200,14 +13029,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.DeleteProjectResp" + "$ref": "#/definitions/internal_handler_tool.PodIngressResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_GpuAnalysisStatusResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport": { "type": "object", "properties": { "code": { @@ -11215,14 +13044,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.GpuAnalysisStatusResp" + "$ref": "#/definitions/internal_handler_tool.PodNodeport" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_JobResourceSummaryResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp": { "type": "object", "properties": { "code": { @@ -11230,14 +13059,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryResp" + "$ref": "#/definitions/internal_handler_tool.PodNodeportResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LLMConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp": { "type": "object", "properties": { "code": { @@ -11245,14 +13074,14 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.LLMConfigResp" + "$ref": "#/definitions/internal_handler_vcjob.JupyterTokenResp" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LoginResp": { + "github_com_raids-lab_crater_internal_resputil.Response-map_string_string": { "type": "object", "properties": { "code": { @@ -11260,615 +13089,838 @@ "type": "integer" }, "data": { - "$ref": "#/definitions/internal_handler.LoginResp" + "$ref": "#/definitions/map_string_string" }, "msg": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadLimitConfigResp": { + "github_com_raids-lab_crater_internal_resputil.Response-string": { "type": "object", "properties": { "code": { "description": "依然保持 int (ErrorCode) 类型", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadLimitConfigResp" + "data": { + "type": "string" + }, + "msg": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult": { + "type": "object", + "properties": { + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitDetail" + } + }, + "enabled": { + "type": "boolean" + }, + "exceeded": { + "type": "boolean" + } + } + }, + "github_com_raids-lab_crater_internal_service.ResourceLimitDetail": { + "type": "object", + "properties": { + "exceeded": { + "type": "boolean" + }, + "limit": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "used": { + "type": "string" + } + } + }, + "github_com_raids-lab_crater_internal_service_vcjob.ForwardType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "_", + "IngressType", + "NodePortType" + ] + }, + "github_com_raids-lab_crater_internal_util.VolumeMount": { + "type": "object", + "properties": { + "datasetID": { + "type": "integer" + }, + "mountPath": { + "type": "string" + }, + "subPath": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_internal_util.VolumeType" + } + } + }, + "github_com_raids-lab_crater_internal_util.VolumeType": { + "type": "integer", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "_", + "FileType", + "DataType" + ] + }, + "github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "allocatable": { + "$ref": "#/definitions/v1.ResourceList" + }, + "arch": { + "type": "string" + }, + "capacity": { + "$ref": "#/definitions/v1.ResourceList" + }, + "containerRuntimeVersion": { + "type": "string" + }, + "gpuArch": { + "type": "string" + }, + "gpuCount": { + "type": "integer" + }, + "gpuDriver": { + "type": "string" + }, + "gpuMemory": { + "type": "string" + }, + "kernelVersion": { + "type": "string" + }, + "kubeletVersion": { + "type": "string" + }, + "name": { + "type": "string" }, - "msg": { + "os": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadListResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadListResp" + "osVersion": { + "type": "string" }, - "msg": { + "role": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ModelDownloadResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ModelDownloadResp" + "status": { + "$ref": "#/definitions/v1.NodeConditionType" }, - "msg": { + "taint": { + "type": "string" + }, + "time": { "type": "string" + }, + "used": { + "$ref": "#/definitions/v1.ResourceList" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_NodeMark": { + "github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "arch": { + "description": "架构", + "type": "string" + }, + "count": { + "description": "数量", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.NodeMark" + "driver": { + "description": "驱动版本", + "type": "string" }, - "msg": { + "label": { + "description": "显示名称(从数据库获取,如 \"NVIDIA GPU\")", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PodBandwidthConfigResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PodBandwidthConfigResp" + "memory": { + "description": "显存", + "type": "string" }, - "msg": { + "product": { + "description": "具体型号(从节点标签获取,如 \"Tesla V100\",可选)", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueConfigResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PrequeueConfigResp" + "resourceName": { + "description": "资源名称,如 \"nvidia.com/gpu\"", + "type": "string" }, - "msg": { + "runtimeVersion": { + "description": "运行时版本(CUDA/ROCm 等)", + "type": "string" + }, + "vendorDomain": { + "description": "供应商域名,如 \"nvidia.com\"", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PrequeueFeatureStatusResp": { + "github_com_raids-lab_crater_pkg_crclient.GPUInfo": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "cudaVersion": { + "type": "string" + }, + "gpuArch": { + "type": "string" + }, + "gpuCount": { + "description": "总 GPU 数量", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PrequeueFeatureStatusResp" + "gpuDevices": { + "description": "多种类型的 GPU 设备列表", + "type": "array", + "items": { + "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo" + } }, - "msg": { + "gpuDriver": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_ProjectCreateResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.ProjectCreateResp" + "gpuMemory": { + "description": "以下字段保留用于向后兼容(取第一个 GPU 设备的信息)", + "type": "string" }, - "msg": { + "gpuProduct": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_PutUserInProjectResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.PutUserInProjectResp" + "gpuUtil": { + "type": "object", + "additionalProperties": { + "type": "number", + "format": "float32" + } }, - "msg": { + "haveGPU": { + "type": "boolean" + }, + "name": { "type": "string" + }, + "relateJobs": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaConfigItemResp": { + "github_com_raids-lab_crater_pkg_crclient.Pod": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "accountID": { + "description": "账户ID(用于跳转)", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.QueueQuotaConfigItemResp" + "accountName": { + "description": "账户昵称(用于显示)", + "type": "string" }, - "msg": { + "accountRealName": { + "description": "账户真实名称(用于tooltip)", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_QueueQuotaResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.QueueQuotaResp" + "createTime": { + "type": "string" }, - "msg": { + "ip": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TokenReq": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.TokenReq" + "locked": { + "type": "boolean" }, - "msg": { + "lockedTimestamp": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_TriggerAllJobsAnalysisResponse": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.TriggerAllJobsAnalysisResponse" + "name": { + "type": "string" }, - "msg": { + "namespace": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserBanStatusResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.UserBanStatusResp" + "ownerReference": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.OwnerReference" + } + }, + "permanentLocked": { + "type": "boolean" + }, + "requestResources": { + "description": "Requests", + "allOf": [ + { + "$ref": "#/definitions/v1.ResourceList" + } + ] + }, + "resources": { + "description": "Limits", + "allOf": [ + { + "$ref": "#/definitions/v1.ResourceList" + } + ] + }, + "status": { + "$ref": "#/definitions/v1.PodPhase" }, - "msg": { - "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_UserDetailResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "userID": { + "description": "用户ID(用于跳转)", "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler.UserDetailResp" + "userName": { + "description": "管理员接口返回的字段(omitempty 表示字段为空时不序列化)", + "type": "string" }, - "msg": { + "userRealName": { + "description": "用户真实名称(用于tooltip)", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_VisibleUserBanStatusResp": { + "gorm.DeletedAt": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" - }, - "data": { - "$ref": "#/definitions/internal_handler.VisibleUserBanStatusResp" - }, - "msg": { + "time": { "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngress": { + "internal_handler.AccountContext": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" + "accessPublic": { + "description": "User access mode of the platform", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" + } + ] }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodIngress" + "accessQueue": { + "description": "User access mode of the queue", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" + } + ] }, - "msg": { + "queue": { + "description": "Current Queue Name", "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodIngressResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodIngressResp" + "rolePlatform": { + "description": "User role of the platform", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + } + ] }, - "msg": { + "roleQueue": { + "description": "User role of the queue", + "allOf": [ + { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + } + ] + }, + "space": { + "description": "User pvc subpath the platform", "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeport": { + "internal_handler.AccountResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" - }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodNodeport" + "access": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" }, - "msg": { + "expiredAt": { "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_tool_PodNodeportResp": { - "type": "object", - "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_tool.PodNodeportResp" + "name": { + "type": "string" }, - "msg": { + "nickname": { "type": "string" + }, + "role": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-internal_handler_vcjob_JupyterTokenResp": { + "internal_handler.AdjustUserExtraBalanceReq": { "type": "object", + "required": [ + "delta" + ], "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "delta": { "type": "integer" }, - "data": { - "$ref": "#/definitions/internal_handler_vcjob.JupyterTokenResp" - }, - "msg": { + "reason": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-map_string_string": { + "internal_handler.AdjustUserExtraBalanceResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", - "type": "integer" + "afterBalance": { + "type": "number" }, - "data": { - "$ref": "#/definitions/map_string_string" + "beforeBalance": { + "type": "number" }, - "msg": { + "delta": { + "type": "number" + }, + "userId": { + "type": "integer" + }, + "username": { "type": "string" } } }, - "github_com_raids-lab_crater_internal_resputil.Response-string": { + "internal_handler.AdminModelDownloadLimitConfigResp": { "type": "object", "properties": { - "code": { - "description": "依然保持 int (ErrorCode) 类型", + "enabled": { + "type": "boolean" + }, + "maxConcurrent": { "type": "integer" }, - "data": { - "type": "string" + "maxSuccessfulDownloads": { + "type": "integer" }, - "msg": { - "type": "string" - } - } - }, - "github_com_raids-lab_crater_internal_service.ResourceLimitCheckResult": { - "type": "object", - "properties": { - "details": { + "whitelistUserIds": { "type": "array", "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_service.ResourceLimitDetail" + "type": "integer" } }, - "enabled": { - "type": "boolean" - }, - "exceeded": { - "type": "boolean" + "windowHours": { + "type": "integer" } } }, - "github_com_raids-lab_crater_internal_service.ResourceLimitDetail": { + "internal_handler.ApprovalOrderResp": { "type": "object", "properties": { - "exceeded": { - "type": "boolean" + "content": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderContent" }, - "limit": { + "createdAt": { "type": "string" }, - "resource": { + "creator": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "creatorID": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { "type": "string" }, - "used": { + "reviewNotes": { "type": "string" + }, + "reviewer": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "reviewerID": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + }, + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" } } }, - "github_com_raids-lab_crater_internal_service_vcjob.ForwardType": { - "type": "integer", - "enum": [ - 0, - 1, - 2 - ], - "x-enum-varnames": [ - "_", - "IngressType", - "NodePortType" - ] - }, - "github_com_raids-lab_crater_internal_util.VolumeMount": { + "internal_handler.ApprovalOrderreq": { "type": "object", + "required": [ + "name", + "type" + ], "properties": { - "datasetID": { + "approvalOrderExtensionHours": { + "description": "延长小时数", "type": "integer" }, - "mountPath": { + "approvalOrderReason": { + "description": "审批原因", "type": "string" }, - "subPath": { + "approvalorderTypeID": { + "description": "关联的ID,可能是数据集或任务ID", + "type": "integer" + }, + "name": { "type": "string" }, + "status": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + }, "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_internal_util.VolumeType" + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" } } }, - "github_com_raids-lab_crater_internal_util.VolumeType": { - "type": "integer", + "internal_handler.AuthMethod": { + "type": "string", "enum": [ - 0, - 1, - 2 + "normal", + "ldap" ], "x-enum-varnames": [ - "_", - "FileType", - "DataType" + "AuthMethodNormal", + "AuthMethodLDAP" ] }, - "github_com_raids-lab_crater_pkg_crclient.ClusterNodeDetail": { + "internal_handler.AuthModeResp": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "allocatable": { - "$ref": "#/definitions/v1.ResourceList" + "enableLdap": { + "type": "boolean" }, - "arch": { - "type": "string" + "enableNormalLogin": { + "type": "boolean" }, - "capacity": { - "$ref": "#/definitions/v1.ResourceList" + "enableNormalRegister": { + "type": "boolean" }, - "containerRuntimeVersion": { + "ldapAlias": { "type": "string" }, - "gpuArch": { + "ldapHelp": { "type": "string" - }, - "gpuCount": { + } + } + }, + "internal_handler.CLICompatibilityInfo": { + "type": "object", + "properties": { + "apiVersion": { "type": "integer" }, - "gpuDriver": { - "type": "string" - }, - "gpuMemory": { + "appVersion": { "type": "string" }, - "kernelVersion": { + "buildTime": { "type": "string" }, - "kubeletVersion": { + "buildType": { "type": "string" }, - "name": { - "type": "string" + "minSupportedCliApiVersion": { + "type": "integer" }, - "os": { + "shortCommitSHA": { "type": "string" - }, - "osVersion": { + } + } + }, + "internal_handler.ChatMessage": { + "type": "object", + "properties": { + "content": { "type": "string" }, "role": { "type": "string" + } + } + }, + "internal_handler.CheckResp": { + "type": "object", + "properties": { + "context": { + "$ref": "#/definitions/internal_handler.AccountContext" }, - "status": { - "$ref": "#/definitions/v1.NodeConditionType" + "user": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute" }, - "taint": { + "version": { + "$ref": "#/definitions/internal_handler.VersionInfo" + } + } + }, + "internal_handler.CreateDownloadReq": { + "type": "object", + "required": [ + "category", + "name" + ], + "properties": { + "category": { + "type": "string", + "enum": [ + "model", + "dataset" + ] + }, + "name": { "type": "string" }, - "time": { + "revision": { "type": "string" }, - "used": { - "$ref": "#/definitions/v1.ResourceList" + "source": { + "type": "string" + }, + "token": { + "description": "Token is an optional access token for gated/private repositories on the\nsource site. It is only forwarded to the download Job as an env var and is\nnever persisted on the (shared, deduplicated) download record.", + "type": "string" } } }, - "github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo": { + "internal_handler.CreateKthenaReq": { "type": "object", + "required": [ + "name", + "worker" + ], "properties": { - "arch": { - "description": "架构", + "backendType": { "type": "string" }, - "count": { - "description": "数量", - "type": "integer" - }, - "driver": { - "description": "驱动版本", + "cacheURI": { "type": "string" }, - "label": { - "description": "显示名称(从数据库获取,如 \"NVIDIA GPU\")", + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "modelSource": { "type": "string" }, - "memory": { - "description": "显存", + "modelURI": { "type": "string" }, - "product": { - "description": "具体型号(从节点标签获取,如 \"Tesla V100\",可选)", + "name": { "type": "string" }, - "resourceName": { - "description": "资源名称,如 \"nvidia.com/gpu\"", + "platformModelId": { + "type": "integer" + }, + "replicas": { + "type": "integer" + }, + "selectors": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NodeSelectorRequirement" + } + }, + "servedModel": { "type": "string" }, - "runtimeVersion": { - "description": "运行时版本(CUDA/ROCm 等)", + "tolerations": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Toleration" + } + }, + "worker": { + "$ref": "#/definitions/internal_handler.KthenaWorkerReq" + } + } + }, + "internal_handler.CurrentUserBanStatusResp": { + "type": "object", + "properties": { + "banRestrictions": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserBanRestrictions" + }, + "banned": { + "type": "boolean" + }, + "bannedTimestamp": { "type": "string" }, - "vendorDomain": { - "description": "供应商域名,如 \"nvidia.com\"", + "permanentBanned": { + "type": "boolean" + }, + "reason": { "type": "string" } } }, - "github_com_raids-lab_crater_pkg_crclient.GPUInfo": { + "internal_handler.DatasetReq": { "type": "object", + "required": [ + "describe", + "name", + "url" + ], "properties": { - "cudaVersion": { + "describe": { "type": "string" }, - "gpuArch": { - "type": "string" + "editable": { + "type": "boolean" }, - "gpuCount": { - "description": "总 GPU 数量", - "type": "integer" + "ispublic": { + "type": "boolean" }, - "gpuDevices": { - "description": "多种类型的 GPU 设备列表", + "name": { + "type": "string" + }, + "tags": { "type": "array", "items": { - "$ref": "#/definitions/github_com_raids-lab_crater_pkg_crclient.GPUDeviceInfo" + "type": "string" } }, - "gpuDriver": { - "type": "string" + "type": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.DataType" }, - "gpuMemory": { - "description": "以下字段保留用于向后兼容(取第一个 GPU 设备的信息)", + "url": { "type": "string" }, - "gpuProduct": { + "weburl": { + "type": "string" + } + } + }, + "internal_handler.DeleteProjectResp": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "internal_handler.DownloadActionReq": { + "type": "object", + "properties": { + "revision": { + "description": "Revision is optional and only used by retry. A non-nil empty value means\n\"use the source default branch\" while preserving the failed record's path.", "type": "string" }, - "gpuUtil": { - "type": "object", - "additionalProperties": { - "type": "number", - "format": "float32" - } - }, - "haveGPU": { + "token": { + "type": "string" + } + } + }, + "internal_handler.FilePermission": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "_", + "NotAllowed", + "ReadOnly", + "ReadWrite" + ] + }, + "internal_handler.GpuAnalysisStatusResp": { + "type": "object", + "properties": { + "enabled": { "type": "boolean" - }, - "name": { - "type": "string" - }, - "relateJobs": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - } } } }, - "github_com_raids-lab_crater_pkg_crclient.Pod": { + "internal_handler.GpuAnalysisWithJobInfo": { "type": "object", "properties": { - "accountID": { - "description": "账户ID(用于跳转)", - "type": "integer" + "UserNickname": { + "type": "string" }, - "accountName": { - "description": "账户昵称(用于显示)", + "command": { + "description": "采集到的原始数据", "type": "string" }, - "accountRealName": { - "description": "账户真实名称(用于tooltip)", + "createdAt": { + "description": "自动追踪的时间戳", "type": "string" }, - "createTime": { + "deletedAt": { + "$ref": "#/definitions/gorm.DeletedAt" + }, + "historicalMetrics": { "type": "string" }, - "ip": { + "id": { + "type": "integer" + }, + "jobID": { + "type": "integer" + }, + "jobName": { "type": "string" }, - "locked": { - "type": "boolean" + "jobType": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.JobType" + }, + "llmversion": { + "type": "string" }, "lockedTimestamp": { "type": "string" @@ -11879,486 +13931,494 @@ "namespace": { "type": "string" }, - "ownerReference": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.OwnerReference" - } + "nodes": { + "$ref": "#/definitions/datatypes.JSONType-array_string" }, - "permanentLocked": { - "type": "boolean" + "phase1LLMReason": { + "type": "string" }, - "requestResources": { - "description": "Requests", - "allOf": [ - { - "$ref": "#/definitions/v1.ResourceList" - } - ] + "phase1Score": { + "description": "LLM 分析结果", + "type": "integer" + }, + "phase2LLMReason": { + "type": "string" + }, + "phase2Score": { + "type": "integer" + }, + "podName": { + "description": "原始 Kubernetes 信息", + "type": "string" }, "resources": { - "description": "Limits", + "$ref": "#/definitions/datatypes.JSONType-v1_ResourceList" + }, + "reviewStatus": { + "description": "管理状态", "allOf": [ { - "$ref": "#/definitions/v1.ResourceList" + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" } ] }, "status": { - "$ref": "#/definitions/v1.PodPhase" + "$ref": "#/definitions/volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase" }, "userID": { - "description": "用户ID(用于跳转)", "type": "integer" }, "userName": { - "description": "管理员接口返回的字段(omitempty 表示字段为空时不序列化)", - "type": "string" - }, - "userRealName": { - "description": "用户真实名称(用于tooltip)", "type": "string" } } }, - "gorm.DeletedAt": { + "internal_handler.JobResourceSummaryAcceleratorResp": { "type": "object", "properties": { - "time": { + "limit": { "type": "string" }, - "valid": { - "description": "Valid is true if Time is not NULL", - "type": "boolean" + "pending": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "running": { + "type": "string" + }, + "used": { + "type": "string" } } }, - "internal_handler.AccountContext": { + "internal_handler.JobResourceSummaryResp": { "type": "object", "properties": { - "accessPublic": { - "description": "User access mode of the platform", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - } - ] - }, - "accessQueue": { - "description": "User access mode of the queue", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - } - ] + "accelerators": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryAcceleratorResp" + } }, - "queue": { - "description": "Current Queue Name", - "type": "string" + "cpu": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" }, - "rolePlatform": { - "description": "User role of the platform", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" - } - ] + "memory": { + "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" }, - "roleQueue": { - "description": "User role of the queue", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" - } - ] + "pendingJobs": { + "type": "integer" }, - "space": { - "description": "User pvc subpath the platform", - "type": "string" + "runningJobs": { + "type": "integer" } } }, - "internal_handler.AccountResp": { + "internal_handler.JobResourceSummaryUsageResp": { "type": "object", "properties": { - "access": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.AccessMode" - }, - "expiredAt": { + "limit": { "type": "string" }, - "name": { + "pending": { "type": "string" }, - "nickname": { + "running": { "type": "string" }, - "role": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.Role" + "used": { + "type": "string" } } }, - "internal_handler.AdjustUserExtraBalanceReq": { + "internal_handler.JobTemplateReq": { "type": "object", "required": [ - "delta" + "name", + "template" ], "properties": { - "delta": { - "type": "integer" + "describe": { + "type": "string" }, - "reason": { + "document": { + "type": "string" + }, + "name": { + "type": "string" + }, + "template": { "type": "string" } } }, - "internal_handler.AdjustUserExtraBalanceResp": { + "internal_handler.KthenaAccess": { "type": "object", "properties": { - "afterBalance": { - "type": "number" + "internalBaseURL": { + "type": "string" }, - "beforeBalance": { - "type": "number" + "modelName": { + "type": "string" }, - "delta": { - "type": "number" + "nodePortURL": { + "type": "string" }, - "userId": { - "type": "integer" + "proxyBaseURL": { + "type": "string" }, - "username": { + "routeName": { + "type": "string" + }, + "routerService": { + "type": "string" + }, + "serverName": { "type": "string" } } }, - "internal_handler.AdminModelDownloadLimitConfigResp": { + "internal_handler.KthenaConversationCreateReq": { "type": "object", "properties": { - "enabled": { - "type": "boolean" - }, - "maxConcurrent": { - "type": "integer" - }, - "maxSuccessfulDownloads": { - "type": "integer" - }, - "whitelistUserIds": { + "messages": { "type": "array", "items": { - "type": "integer" + "$ref": "#/definitions/internal_handler.KthenaConversationMessageReq" } }, - "windowHours": { + "sessionId": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "internal_handler.KthenaConversationMessageReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "role": { + "type": "string" + } + } + }, + "internal_handler.KthenaConversationMessageResp": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "role": { + "type": "string" + }, + "sequence": { "type": "integer" } } }, - "internal_handler.ApprovalOrderResp": { + "internal_handler.KthenaConversationResp": { "type": "object", "properties": { - "content": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderContent" + "backendType": { + "type": "string" }, "createdAt": { "type": "string" }, - "creator": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" - }, - "creatorID": { + "messageCount": { "type": "integer" }, - "id": { - "type": "integer" + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageResp" + } }, - "name": { + "modelName": { "type": "string" }, - "reviewNotes": { + "namespace": { "type": "string" }, - "reviewer": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + "serviceName": { + "type": "string" }, - "reviewerID": { - "type": "integer" + "sessionId": { + "type": "string" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" + "title": { + "type": "string" }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" + "updatedAt": { + "type": "string" } } }, - "internal_handler.ApprovalOrderreq": { + "internal_handler.KthenaConversationTurnReq": { "type": "object", - "required": [ - "name", - "type" - ], "properties": { - "approvalOrderExtensionHours": { - "description": "延长小时数", - "type": "integer" + "clientTurnId": { + "type": "string" }, - "approvalOrderReason": { - "description": "审批原因", + "content": { "type": "string" }, - "approvalorderTypeID": { - "description": "关联的ID,可能是数据集或任务ID", + "maxTokens": { "type": "integer" }, - "name": { + "sessionId": { "type": "string" }, - "status": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus" - }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderType" + "temperature": { + "type": "number" } } }, - "internal_handler.AuthMethod": { - "type": "string", - "enum": [ - "normal", - "ldap" - ], - "x-enum-varnames": [ - "AuthMethodNormal", - "AuthMethodLDAP" - ] - }, - "internal_handler.AuthModeResp": { + "internal_handler.KthenaConversationTurnResp": { "type": "object", "properties": { - "enableLdap": { - "type": "boolean" - }, - "enableNormalLogin": { - "type": "boolean" + "assistant": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageResp" }, - "enableNormalRegister": { - "type": "boolean" + "completion": { + "type": "object" }, - "ldapAlias": { - "type": "string" + "conversation": { + "$ref": "#/definitions/internal_handler.KthenaConversationResp" + } + } + }, + "internal_handler.KthenaConversationUpdateReq": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaConversationMessageReq" + } }, - "ldapHelp": { + "title": { "type": "string" } } }, - "internal_handler.CLICompatibilityInfo": { + "internal_handler.KthenaDiagnostic": { "type": "object", "properties": { - "apiVersion": { - "type": "integer" + "container": { + "type": "string" }, - "appVersion": { + "details": { "type": "string" }, - "buildTime": { + "level": { "type": "string" }, - "buildType": { + "message": { "type": "string" }, - "minSupportedCliApiVersion": { - "type": "integer" + "pod": { + "type": "string" }, - "shortCommitSHA": { + "reason": { + "type": "string" + }, + "resource": { + "type": "string" + }, + "timestamp": { "type": "string" } } }, - "internal_handler.CheckResp": { + "internal_handler.KthenaInferenceStatusResp": { "type": "object", "properties": { - "context": { - "$ref": "#/definitions/internal_handler.AccountContext" - }, - "user": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute" - }, - "version": { - "$ref": "#/definitions/internal_handler.VersionInfo" + "enabled": { + "type": "boolean" } } }, - "internal_handler.CreateDownloadReq": { + "internal_handler.KthenaInferenceTemplateReq": { "type": "object", "required": [ - "category", + "config", "name" ], "properties": { - "category": { - "type": "string", - "enum": [ - "model", - "dataset" - ] + "config": { + "type": "object" + }, + "description": { + "type": "string" }, "name": { "type": "string" + } + } + }, + "internal_handler.KthenaInferenceTemplateResp": { + "type": "object", + "properties": { + "config": { + "type": "object" }, - "revision": { + "createdAt": { "type": "string" }, - "source": { + "description": { "type": "string" }, - "token": { - "description": "Token is an optional access token for gated/private repositories on the\nsource site. It is only forwarded to the download Job as an env var and is\nnever persisted on the (shared, deduplicated) download record.", + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "updatedAt": { "type": "string" } } }, - "internal_handler.CurrentUserBanStatusResp": { + "internal_handler.KthenaProxyReq": { "type": "object", "properties": { - "banRestrictions": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserBanRestrictions" + "max_tokens": { + "type": "integer" }, - "banned": { - "type": "boolean" + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.ChatMessage" + } }, - "bannedTimestamp": { + "model": { "type": "string" }, - "permanentBanned": { + "prompt": {}, + "stream": { "type": "boolean" }, - "reason": { - "type": "string" + "temperature": { + "type": "number" } } }, - "internal_handler.DatasetReq": { + "internal_handler.KthenaResource": { "type": "object", - "required": [ - "describe", - "name", - "url" - ], "properties": { - "describe": { - "type": "string" - }, - "editable": { - "type": "boolean" - }, - "ispublic": { - "type": "boolean" - }, - "name": { - "type": "string" - }, - "tags": { + "conditions": { "type": "array", "items": { - "type": "string" + "type": "object", + "additionalProperties": {} } }, - "type": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.DataType" + "kind": { + "type": "string" }, - "url": { + "name": { "type": "string" }, - "weburl": { + "namespace": { + "type": "string" + }, + "phase": { "type": "string" + }, + "ready": { + "type": "boolean" } } }, - "internal_handler.DeleteProjectResp": { + "internal_handler.KthenaRuntimePod": { "type": "object", - "required": [ - "name" - ], "properties": { + "hostIP": { + "type": "string" + }, "name": { "type": "string" - } - } - }, - "internal_handler.DownloadActionReq": { - "type": "object", - "properties": { - "revision": { - "description": "Revision is optional and only used by retry. A non-nil empty value means\n\"use the source default branch\" while preserving the failed record's path.", + }, + "namespace": { "type": "string" }, - "token": { + "nodeName": { "type": "string" - } - } - }, - "internal_handler.FilePermission": { - "type": "integer", - "enum": [ - 0, - 1, - 2, - 3 - ], - "x-enum-varnames": [ - "_", - "NotAllowed", - "ReadOnly", - "ReadWrite" - ] - }, - "internal_handler.GpuAnalysisStatusResp": { - "type": "object", - "properties": { - "enabled": { + }, + "phase": { + "type": "string" + }, + "podIP": { + "type": "string" + }, + "ready": { "type": "boolean" + }, + "readyContainers": { + "type": "integer" + }, + "restarts": { + "type": "integer" + }, + "totalContainers": { + "type": "integer" } } }, - "internal_handler.GpuAnalysisWithJobInfo": { + "internal_handler.KthenaServiceResp": { "type": "object", "properties": { - "UserNickname": { - "type": "string" + "access": { + "$ref": "#/definitions/internal_handler.KthenaAccess" }, - "command": { - "description": "采集到的原始数据", + "backendType": { "type": "string" }, - "createdAt": { - "description": "自动追踪的时间戳", + "cacheURI": { "type": "string" }, - "deletedAt": { - "$ref": "#/definitions/gorm.DeletedAt" + "conditions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {} + } }, - "historicalMetrics": { + "createdAt": { "type": "string" }, - "id": { - "type": "integer" - }, - "jobID": { - "type": "integer" + "diagnostics": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaDiagnostic" + } }, - "jobName": { - "type": "string" + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "jobType": { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.JobType" + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } }, - "llmversion": { + "modelSource": { "type": "string" }, - "lockedTimestamp": { + "modelURI": { "type": "string" }, "name": { @@ -12367,126 +14427,94 @@ "namespace": { "type": "string" }, - "nodes": { - "$ref": "#/definitions/datatypes.JSONType-array_string" - }, - "phase1LLMReason": { + "owner": { "type": "string" }, - "phase1Score": { - "description": "LLM 分析结果", - "type": "integer" - }, - "phase2LLMReason": { + "phase": { "type": "string" }, - "phase2Score": { + "platformModelId": { "type": "integer" }, - "podName": { - "description": "原始 Kubernetes 信息", - "type": "string" + "replicas": { + "type": "integer" }, "resources": { - "$ref": "#/definitions/datatypes.JSONType-v1_ResourceList" + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaResource" + } }, - "reviewStatus": { - "description": "管理状态", - "allOf": [ - { - "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ReviewStatus" - } - ] + "runtimePods": { + "type": "array", + "items": { + "$ref": "#/definitions/internal_handler.KthenaRuntimePod" + } }, - "status": { - "$ref": "#/definitions/volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase" + "servedModel": { + "type": "string" }, - "userID": { - "type": "integer" + "userInfo": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" }, - "userName": { - "type": "string" - } - } - }, - "internal_handler.JobResourceSummaryAcceleratorResp": { - "type": "object", - "properties": { - "limit": { + "workerCPU": { "type": "string" }, - "pending": { + "workerConfig": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "workerGPU": { "type": "string" }, - "resource": { + "workerGPUModel": { "type": "string" }, - "running": { + "workerImage": { "type": "string" }, - "used": { + "workerMemory": { "type": "string" + }, + "workerReplicas": { + "type": "integer" } } }, - "internal_handler.JobResourceSummaryResp": { + "internal_handler.KthenaWorkerReq": { "type": "object", + "required": [ + "image" + ], "properties": { - "accelerators": { - "type": "array", - "items": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryAcceleratorResp" + "config": { + "type": "object", + "additionalProperties": { + "type": "string" } }, "cpu": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" - }, - "memory": { - "$ref": "#/definitions/internal_handler.JobResourceSummaryUsageResp" - }, - "pendingJobs": { - "type": "integer" - }, - "runningJobs": { - "type": "integer" - } - } - }, - "internal_handler.JobResourceSummaryUsageResp": { - "type": "object", - "properties": { - "limit": { "type": "string" }, - "pending": { + "gpu": { "type": "string" }, - "running": { + "gpuModel": { "type": "string" }, - "used": { - "type": "string" - } - } - }, - "internal_handler.JobTemplateReq": { - "type": "object", - "required": [ - "name", - "template" - ], - "properties": { - "describe": { + "image": { "type": "string" }, - "document": { + "memory": { "type": "string" }, - "name": { - "type": "string" + "pods": { + "type": "integer" }, - "template": { - "type": "string" + "replicas": { + "type": "integer" } } }, @@ -12970,6 +14998,17 @@ } } }, + "internal_handler.SetKthenaInferenceStatusReq": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean" + } + } + }, "internal_handler.SharedQueueReq": { "type": "object", "required": [ @@ -14114,6 +16153,89 @@ } } }, + "internal_handler_vcjob.WorkloadResp": { + "type": "object", + "properties": { + "billedPointsTotal": { + "type": "number" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "detailPath": { + "type": "string" + }, + "jobName": { + "type": "string" + }, + "jobType": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "lockedTimestamp": { + "type": "string" + }, + "model": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "owner": { + "type": "string" + }, + "permanentLocked": { + "type": "boolean" + }, + "queue": { + "type": "string" + }, + "resources": { + "$ref": "#/definitions/v1.ResourceList" + }, + "scheduleType": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.ScheduleType" + }, + "scheduler": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "status": { + "type": "string" + }, + "statusDetail": { + "type": "string" + }, + "userInfo": { + "$ref": "#/definitions/github_com_raids-lab_crater_dao_model.UserInfo" + }, + "waitingToleranceSeconds": { + "type": "integer" + }, + "workloadID": { + "type": "string" + }, + "workloadKind": { + "type": "string" + } + } + }, "map_string_string": { "type": "object", "additionalProperties": { @@ -14386,6 +16508,63 @@ } } }, + "v1.TaintEffect": { + "type": "string", + "enum": [ + "NoSchedule", + "PreferNoSchedule", + "NoExecute" + ], + "x-enum-varnames": [ + "TaintEffectNoSchedule", + "TaintEffectPreferNoSchedule", + "TaintEffectNoExecute" + ] + }, + "v1.Toleration": { + "type": "object", + "properties": { + "effect": { + "description": "Effect indicates the taint effect to match. Empty means match all taint effects.\nWhen specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.\n+optional", + "allOf": [ + { + "$ref": "#/definitions/v1.TaintEffect" + } + ] + }, + "key": { + "description": "Key is the taint key that the toleration applies to. Empty means match all taint keys.\nIf the key is empty, operator must be Exists; this combination means to match all values and all keys.\n+optional", + "type": "string" + }, + "operator": { + "description": "Operator represents a key's relationship to the value.\nValid operators are Exists and Equal. Defaults to Equal.\nExists is equivalent to wildcard for value, so that a pod can\ntolerate all taints of a particular category.\n+optional", + "allOf": [ + { + "$ref": "#/definitions/v1.TolerationOperator" + } + ] + }, + "tolerationSeconds": { + "description": "TolerationSeconds represents the period of time the toleration (which must be\nof effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,\nit is not set, which means tolerate the taint forever (do not evict). Zero and\nnegative values will be treated as 0 (evict immediately) by the system.\n+optional", + "type": "integer" + }, + "value": { + "description": "Value is the taint value the toleration matches to.\nIf the operator is Exists, the value should be empty, otherwise just a regular string.\n+optional", + "type": "string" + } + } + }, + "v1.TolerationOperator": { + "type": "string", + "enum": [ + "Exists", + "Equal" + ], + "x-enum-varnames": [ + "TolerationOpExists", + "TolerationOpEqual" + ] + }, "volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase": { "type": "string", "enum": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index fd6e4fd3d..10b8c43f3 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -437,6 +437,19 @@ definitions: total: type: integer type: object + github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp: + properties: + items: + items: + $ref: '#/definitions/internal_handler_vcjob.WorkloadResp' + type: array + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object github_com_raids-lab_crater_internal_resputil.Response-any: properties: code: @@ -494,6 +507,42 @@ definitions: msg: type: string type: object + github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + items: + $ref: '#/definitions/internal_handler.KthenaConversationResp' + type: array + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + items: + $ref: '#/definitions/internal_handler.KthenaInferenceTemplateResp' + type: array + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + items: + $ref: '#/definitions/internal_handler.KthenaServiceResp' + type: array + msg: + type: string + type: object github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_ModelDownloadResp: properties: code: @@ -556,6 +605,16 @@ definitions: msg: type: string type: object + ? github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp + : properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Page-internal_handler_vcjob_WorkloadResp' + msg: + type: string + type: object github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_service_ResourceLimitCheckResult: properties: code: @@ -686,6 +745,56 @@ definitions: msg: type: string type: object + github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/internal_handler.KthenaConversationResp' + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/internal_handler.KthenaConversationTurnResp' + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/internal_handler.KthenaInferenceStatusResp' + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/internal_handler.KthenaInferenceTemplateResp' + msg: + type: string + type: object + github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp: + properties: + code: + description: 依然保持 int (ErrorCode) 类型 + type: integer + data: + $ref: '#/definitions/internal_handler.KthenaServiceResp' + msg: + type: string + type: object github_com_raids-lab_crater_internal_resputil.Response-internal_handler_LLMConfigResp: properties: code: @@ -1311,6 +1420,13 @@ definitions: shortCommitSHA: type: string type: object + internal_handler.ChatMessage: + properties: + content: + type: string + role: + type: string + type: object internal_handler.CheckResp: properties: context: @@ -1343,6 +1459,42 @@ definitions: - category - name type: object + internal_handler.CreateKthenaReq: + properties: + backendType: + type: string + cacheURI: + type: string + env: + additionalProperties: + type: string + type: object + modelSource: + type: string + modelURI: + type: string + name: + type: string + platformModelId: + type: integer + replicas: + type: integer + selectors: + items: + $ref: '#/definitions/v1.NodeSelectorRequirement' + type: array + servedModel: + type: string + tolerations: + items: + $ref: '#/definitions/v1.Toleration' + type: array + worker: + $ref: '#/definitions/internal_handler.KthenaWorkerReq' + required: + - name + - worker + type: object internal_handler.CurrentUserBanStatusResp: properties: banRestrictions: @@ -1525,362 +1677,674 @@ definitions: - name - template type: object - internal_handler.LLMConfigResp: + internal_handler.KthenaAccess: properties: - apiKey: - type: string - baseUrl: + internalBaseURL: type: string modelName: type: string - type: object - internal_handler.LinkGPUToVGPUReq: - properties: - description: + nodePortURL: type: string - max: - type: integer - min: - type: integer - vgpuResourceId: - type: integer - required: - - vgpuResourceId - type: object - internal_handler.LinkResourceReq: - properties: - rdmaId: - type: integer - required: - - rdmaId - type: object - internal_handler.LoginReq: - properties: - auth: - allOf: - - $ref: '#/definitions/internal_handler.AuthMethod' - description: '[normal, ldap]' - password: - description: (ldap, normal) + proxyBaseURL: type: string - token: - description: Legacy ACT API token (deprecated) + routeName: type: string - username: - description: (ldap, normal) + routerService: + type: string + serverName: type: string - required: - - auth type: object - internal_handler.LoginResp: + internal_handler.KthenaConversationCreateReq: properties: - accessToken: + messages: + items: + $ref: '#/definitions/internal_handler.KthenaConversationMessageReq' + type: array + sessionId: type: string - context: - $ref: '#/definitions/internal_handler.AccountContext' - refreshToken: + title: type: string - user: - $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute' type: object - internal_handler.ModelDownloadLimitConfigResp: + internal_handler.KthenaConversationMessageReq: properties: - enabled: - type: boolean - exempt: - type: boolean - maxConcurrent: - type: integer - maxSuccessfulDownloads: - type: integer - windowHours: - type: integer + content: + type: string + role: + type: string type: object - internal_handler.ModelDownloadListResp: + internal_handler.KthenaConversationMessageResp: properties: - items: - items: - $ref: '#/definitions/internal_handler.ModelDownloadResp' - type: array - summary: - additionalProperties: - format: int64 - type: integer - type: object - total: + content: + type: string + createdAt: + type: string + role: + type: string + sequence: type: integer type: object - internal_handler.ModelDownloadResp: + internal_handler.KthenaConversationResp: properties: - canDelete: - type: boolean - canManage: - type: boolean - canViewLogs: - type: boolean - category: + backendType: type: string createdAt: type: string - creatorId: + messageCount: type: integer - displayName: - type: string - downloadSpeed: + messages: + items: + $ref: '#/definitions/internal_handler.KthenaConversationMessageResp' + type: array + modelName: type: string - downloadedBytes: - type: integer - id: - type: integer - jobName: + namespace: type: string - library: + serviceName: type: string - license: + sessionId: type: string - message: + title: type: string - modelType: + updatedAt: type: string - name: + type: object + internal_handler.KthenaConversationTurnReq: + properties: + clientTurnId: type: string - parameterCount: - type: integer - path: + content: type: string - referenceCount: - description: 'Deprecated: use requesterCount.' + maxTokens: type: integer - relation: + sessionId: type: string - requesterCount: - type: integer - requesters: + temperature: + type: number + type: object + internal_handler.KthenaConversationTurnResp: + properties: + assistant: + $ref: '#/definitions/internal_handler.KthenaConversationMessageResp' + completion: + type: object + conversation: + $ref: '#/definitions/internal_handler.KthenaConversationResp' + type: object + internal_handler.KthenaConversationUpdateReq: + properties: + messages: items: - $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' + $ref: '#/definitions/internal_handler.KthenaConversationMessageReq' type: array - revision: + title: type: string - sizeBytes: - type: integer - source: + type: object + internal_handler.KthenaDiagnostic: + properties: + container: type: string - sourceCreatedAt: + details: type: string - sourceUpdatedAt: + level: type: string - sourceUrl: + message: type: string - status: + pod: type: string - task: + reason: type: string - updatedAt: + resource: + type: string + timestamp: type: string - userInfo: - $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' type: object - internal_handler.NodeAnnotation: + internal_handler.KthenaInferenceStatusResp: properties: - key: + enabled: + type: boolean + type: object + internal_handler.KthenaInferenceTemplateReq: + properties: + config: + type: object + description: type: string - value: + name: type: string + required: + - config + - name type: object - internal_handler.NodeLabel: + internal_handler.KthenaInferenceTemplateResp: properties: - key: + config: + type: object + createdAt: type: string - value: + description: + type: string + id: + type: integer + name: + type: string + updatedAt: type: string type: object - internal_handler.NodeMark: + internal_handler.KthenaProxyReq: properties: - annotations: - items: - $ref: '#/definitions/internal_handler.NodeAnnotation' - type: array - labels: - items: - $ref: '#/definitions/internal_handler.NodeLabel' - type: array - taints: + max_tokens: + type: integer + messages: items: - $ref: '#/definitions/internal_handler.NodeTaint' + $ref: '#/definitions/internal_handler.ChatMessage' type: array - type: object - internal_handler.NodeScheduleRequest: - properties: - reason: - description: 操作原因 + model: type: string + prompt: {} + stream: + type: boolean + temperature: + type: number type: object - internal_handler.NodeTaint: + internal_handler.KthenaResource: properties: - effect: + conditions: + items: + additionalProperties: {} + type: object + type: array + kind: type: string - key: + name: type: string - reason: - description: 操作原因 + namespace: type: string - value: + phase: type: string + ready: + type: boolean type: object - internal_handler.PodBandwidthConfigResp: + internal_handler.KthenaRuntimePod: properties: - capabilityAvailable: - type: boolean - capabilityMessage: + hostIP: type: string - enabled: - type: boolean - jobEgressBandwidth: + name: type: string - jobIngressBandwidth: + namespace: type: string - modelDownloadBandwidth: + nodeName: type: string - type: object - internal_handler.PrequeueConfigResp: - properties: - activateTickerIntervalSeconds: - type: integer - backfillEnabled: + phase: + type: string + podIP: + type: string + ready: type: boolean - maxTotalActivationsPerRound: - type: integer - normalJobWaitingToleranceSeconds: + readyContainers: type: integer - prequeueCandidateSize: + restarts: type: integer - queueQuotaEnabled: - type: boolean - type: object - internal_handler.PrequeueFeatureStatusResp: - properties: - backfillEnabled: - type: boolean - type: object - internal_handler.ProjectCreateResp: - properties: - id: + totalContainers: type: integer type: object - internal_handler.PutUserInProjectReq: + internal_handler.KthenaServiceResp: properties: - accessmode: + access: + $ref: '#/definitions/internal_handler.KthenaAccess' + backendType: type: string - quota: - $ref: '#/definitions/datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota' - role: + cacheURI: type: string - uid: - type: integer - required: - - uid - type: object - internal_handler.PutUserInProjectResp: - properties: - aid: - type: integer - uid: - type: integer - required: - - aid - - uid - type: object - internal_handler.QueueQuotaConfigItemResp: - properties: - id: - type: integer - name: + conditions: + items: + additionalProperties: {} + type: object + type: array + createdAt: type: string - quota: + diagnostics: + items: + $ref: '#/definitions/internal_handler.KthenaDiagnostic' + type: array + env: additionalProperties: type: string type: object - type: object - internal_handler.QueueQuotaReq: - properties: - name: - type: string - quota: + labels: additionalProperties: type: string type: object - type: object - internal_handler.QueueQuotaResp: - properties: - quotas: + modelSource: + type: string + modelURI: + type: string + name: + type: string + namespace: + type: string + owner: + type: string + phase: + type: string + platformModelId: + type: integer + replicas: + type: integer + resources: items: - $ref: '#/definitions/internal_handler.QueueQuotaConfigItemResp' + $ref: '#/definitions/internal_handler.KthenaResource' type: array - type: object - internal_handler.ResourceLimitCheckReq: - properties: - requestedResources: + runtimePods: + items: + $ref: '#/definitions/internal_handler.KthenaRuntimePod' + type: array + servedModel: + type: string + userInfo: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' + workerCPU: + type: string + workerConfig: additionalProperties: type: string type: object + workerGPU: + type: string + workerGPUModel: + type: string + workerImage: + type: string + workerMemory: + type: string + workerReplicas: + type: integer type: object - internal_handler.ReviewApprovalOrderReq: + internal_handler.KthenaWorkerReq: properties: - reviewNotes: + config: + additionalProperties: + type: string + type: object + cpu: type: string - status: - $ref: '#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus' + gpu: + type: string + gpuModel: + type: string + image: + type: string + memory: + type: string + pods: + type: integer + replicas: + type: integer required: - - status + - image type: object - internal_handler.SetGpuAnalysisStatusReq: + internal_handler.LLMConfigResp: properties: - enable: - type: boolean + apiKey: + type: string + baseUrl: + type: string + modelName: + type: string type: object - internal_handler.SharedQueueReq: + internal_handler.LinkGPUToVGPUReq: properties: - datasetID: + description: + type: string + max: + type: integer + min: + type: integer + vgpuResourceId: type: integer - queueIDs: - items: - type: integer - type: array required: - - datasetID - - queueIDs + - vgpuResourceId type: object - internal_handler.SharedUserReq: + internal_handler.LinkResourceReq: properties: - datasetID: + rdmaId: type: integer - userIDs: - items: - type: integer - type: array required: - - datasetID - - userIDs + - rdmaId type: object - internal_handler.SwitchQueueReq: + internal_handler.LoginReq: properties: - queue: + auth: + allOf: + - $ref: '#/definitions/internal_handler.AuthMethod' + description: '[normal, ldap]' + password: + description: (ldap, normal) + type: string + token: + description: Legacy ACT API token (deprecated) + type: string + username: + description: (ldap, normal) type: string required: - - queue + - auth type: object - internal_handler.TokenReq: + internal_handler.LoginResp: properties: - permission: - $ref: '#/definitions/internal_handler.FilePermission' - rootPath: + accessToken: type: string - userId: - type: integer - type: object - internal_handler.TriggerAllJobsAnalysisResponse: - properties: - message: + context: + $ref: '#/definitions/internal_handler.AccountContext' + refreshToken: + type: string + user: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserAttribute' + type: object + internal_handler.ModelDownloadLimitConfigResp: + properties: + enabled: + type: boolean + exempt: + type: boolean + maxConcurrent: + type: integer + maxSuccessfulDownloads: + type: integer + windowHours: + type: integer + type: object + internal_handler.ModelDownloadListResp: + properties: + items: + items: + $ref: '#/definitions/internal_handler.ModelDownloadResp' + type: array + summary: + additionalProperties: + format: int64 + type: integer + type: object + total: + type: integer + type: object + internal_handler.ModelDownloadResp: + properties: + canDelete: + type: boolean + canManage: + type: boolean + canViewLogs: + type: boolean + category: + type: string + createdAt: + type: string + creatorId: + type: integer + displayName: + type: string + downloadSpeed: + type: string + downloadedBytes: + type: integer + id: + type: integer + jobName: + type: string + library: + type: string + license: + type: string + message: + type: string + modelType: + type: string + name: + type: string + parameterCount: + type: integer + path: + type: string + referenceCount: + description: 'Deprecated: use requesterCount.' + type: integer + relation: + type: string + requesterCount: + type: integer + requesters: + items: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' + type: array + revision: + type: string + sizeBytes: + type: integer + source: + type: string + sourceCreatedAt: + type: string + sourceUpdatedAt: + type: string + sourceUrl: + type: string + status: + type: string + task: + type: string + updatedAt: + type: string + userInfo: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' + type: object + internal_handler.NodeAnnotation: + properties: + key: + type: string + value: + type: string + type: object + internal_handler.NodeLabel: + properties: + key: + type: string + value: + type: string + type: object + internal_handler.NodeMark: + properties: + annotations: + items: + $ref: '#/definitions/internal_handler.NodeAnnotation' + type: array + labels: + items: + $ref: '#/definitions/internal_handler.NodeLabel' + type: array + taints: + items: + $ref: '#/definitions/internal_handler.NodeTaint' + type: array + type: object + internal_handler.NodeScheduleRequest: + properties: + reason: + description: 操作原因 + type: string + type: object + internal_handler.NodeTaint: + properties: + effect: + type: string + key: + type: string + reason: + description: 操作原因 + type: string + value: + type: string + type: object + internal_handler.PodBandwidthConfigResp: + properties: + capabilityAvailable: + type: boolean + capabilityMessage: + type: string + enabled: + type: boolean + jobEgressBandwidth: + type: string + jobIngressBandwidth: + type: string + modelDownloadBandwidth: + type: string + type: object + internal_handler.PrequeueConfigResp: + properties: + activateTickerIntervalSeconds: + type: integer + backfillEnabled: + type: boolean + maxTotalActivationsPerRound: + type: integer + normalJobWaitingToleranceSeconds: + type: integer + prequeueCandidateSize: + type: integer + queueQuotaEnabled: + type: boolean + type: object + internal_handler.PrequeueFeatureStatusResp: + properties: + backfillEnabled: + type: boolean + type: object + internal_handler.ProjectCreateResp: + properties: + id: + type: integer + type: object + internal_handler.PutUserInProjectReq: + properties: + accessmode: + type: string + quota: + $ref: '#/definitions/datatypes.JSONType-github_com_raids-lab_crater_dao_model_QueueQuota' + role: + type: string + uid: + type: integer + required: + - uid + type: object + internal_handler.PutUserInProjectResp: + properties: + aid: + type: integer + uid: + type: integer + required: + - aid + - uid + type: object + internal_handler.QueueQuotaConfigItemResp: + properties: + id: + type: integer + name: + type: string + quota: + additionalProperties: + type: string + type: object + type: object + internal_handler.QueueQuotaReq: + properties: + name: + type: string + quota: + additionalProperties: + type: string + type: object + type: object + internal_handler.QueueQuotaResp: + properties: + quotas: + items: + $ref: '#/definitions/internal_handler.QueueQuotaConfigItemResp' + type: array + type: object + internal_handler.ResourceLimitCheckReq: + properties: + requestedResources: + additionalProperties: + type: string + type: object + type: object + internal_handler.ReviewApprovalOrderReq: + properties: + reviewNotes: + type: string + status: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.ApprovalOrderStatus' + required: + - status + type: object + internal_handler.SetGpuAnalysisStatusReq: + properties: + enable: + type: boolean + type: object + internal_handler.SetKthenaInferenceStatusReq: + properties: + enabled: + type: boolean + required: + - enabled + type: object + internal_handler.SharedQueueReq: + properties: + datasetID: + type: integer + queueIDs: + items: + type: integer + type: array + required: + - datasetID + - queueIDs + type: object + internal_handler.SharedUserReq: + properties: + datasetID: + type: integer + userIDs: + items: + type: integer + type: array + required: + - datasetID + - userIDs + type: object + internal_handler.SwitchQueueReq: + properties: + queue: + type: string + required: + - queue + type: object + internal_handler.TokenReq: + properties: + permission: + $ref: '#/definitions/internal_handler.FilePermission' + rootPath: + type: string + userId: + type: integer + type: object + internal_handler.TriggerAllJobsAnalysisResponse: + properties: + message: type: string queuedJobs: type: integer @@ -2602,6 +3066,61 @@ definitions: token: type: string type: object + internal_handler_vcjob.WorkloadResp: + properties: + billedPointsTotal: + type: number + completedAt: + type: string + createdAt: + type: string + detailPath: + type: string + jobName: + type: string + jobType: + type: string + locked: + type: boolean + lockedTimestamp: + type: string + model: + type: string + name: + type: string + namespace: + type: string + nodes: + items: + type: string + type: array + owner: + type: string + permanentLocked: + type: boolean + queue: + type: string + resources: + $ref: '#/definitions/v1.ResourceList' + scheduleType: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.ScheduleType' + scheduler: + type: string + startedAt: + type: string + status: + type: string + statusDetail: + type: string + userInfo: + $ref: '#/definitions/github_com_raids-lab_crater_dao_model.UserInfo' + waitingToleranceSeconds: + type: integer + workloadID: + type: string + workloadKind: + type: string + type: object map_string_string: additionalProperties: type: string @@ -2866,6 +3385,63 @@ definitions: +optional type: boolean type: object + v1.TaintEffect: + enum: + - NoSchedule + - PreferNoSchedule + - NoExecute + type: string + x-enum-varnames: + - TaintEffectNoSchedule + - TaintEffectPreferNoSchedule + - TaintEffectNoExecute + v1.Toleration: + properties: + effect: + allOf: + - $ref: '#/definitions/v1.TaintEffect' + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + +optional + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + +optional + type: string + operator: + allOf: + - $ref: '#/definitions/v1.TolerationOperator' + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + +optional + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + +optional + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + +optional + type: string + type: object + v1.TolerationOperator: + enum: + - Exists + - Equal + type: string + x-enum-varnames: + - TolerationOpExists + - TolerationOpEqual volcano_sh_apis_pkg_apis_batch_v1alpha1.JobPhase: enum: - Pending @@ -4005,9 +4581,123 @@ paths: responses: {} security: - Bearer: [] - summary: 管理员更新镜像的任务类型 + summary: 管理员更新镜像的任务类型 + tags: + - ImagePack + /v1/admin/kthena/inference-services: + get: + consumes: + - application/json + description: List all Kthena inference services managed by Crater. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: List all inference services + tags: + - kthena + /v1/admin/kthena/inference-services/{name}: + delete: + consumes: + - application/json + description: Delete any Kthena inference service managed by Crater. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: Delete inference service as admin + tags: + - kthena + get: + consumes: + - application/json + description: Get any Kthena inference service managed by Crater. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: Get inference service as admin + tags: + - kthena + /v1/admin/kthena/inference-services/{name}/yaml: + get: + consumes: + - application/json + description: Get raw Kthena ModelBooster object for any inference service managed + by Crater. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: Get inference service YAML as admin tags: - - ImagePack + - kthena /v1/admin/models/downloads: get: consumes: @@ -4995,6 +5685,48 @@ paths: summary: 设置 GPU 分析功能开关 tags: - SystemConfig + /v1/admin/system-config/kthena-inference: + get: + description: 查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。 + produces: + - application/json + responses: + "200": + description: 开关状态 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp' + security: + - Bearer: [] + summary: 获取模型部署功能开关状态 + tags: + - SystemConfig + put: + consumes: + - application/json + description: 开启后,用户可以创建和管理基于 Kthena 的在线模型部署;关闭后所有模型部署接口均会拒绝访问。 + parameters: + - description: 开关设置 + in: body + name: data + required: true + schema: + $ref: '#/definitions/internal_handler.SetKthenaInferenceStatusReq' + produces: + - application/json + responses: + "200": + description: 设置成功 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 设置模型部署功能开关 + tags: + - SystemConfig /v1/admin/system-config/llm: delete: description: 清空 LLM 配置(BaseURL, Key, Model)并强制关闭 GPU 分析功能 @@ -6100,7 +6832,223 @@ paths: "200": description: 成功返回值描述 schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 数据集的共享队列 + tags: + - Dataset + /v1/dataset/{datasetId}/queuesNotIn: + get: + consumes: + - application/json + description: 没有该数据集权限的队列列表 + parameters: + - in: path + name: datasetId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 没有该数据集权限的队列列表 + tags: + - Dataset + /v1/dataset/{datasetId}/usersIn: + get: + consumes: + - application/json + description: 获取该数据集共享用户 + parameters: + - in: path + name: datasetId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 获取该数据集共享用户 + tags: + - Dataset + /v1/dataset/{datasetId}/usersNotIn: + get: + consumes: + - application/json + description: 没有该数据集权限的用户列表 + parameters: + - in: path + name: datasetId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 没有该数据集权限的用户列表 + tags: + - Dataset + /v1/dataset/cancelshare/queue: + post: + consumes: + - application/json + description: 普通用户取消数据共享队列 + parameters: + - description: 共享数据集队列 + in: body + name: queueReq + required: true + schema: + $ref: '#/definitions/internal_handler.cancelSharedQueueReq' + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 普通用户取消数据共享队列 + tags: + - Dataset + /v1/dataset/cancelshare/user: + post: + consumes: + - application/json + description: 普通用户取消数据集共享 + parameters: + - description: 共享数据集用户 + in: body + name: Req + required: true + schema: + $ref: '#/definitions/internal_handler.cancelsharedUserReq' + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 普通用户取消数据集共享用户 + tags: + - Dataset + /v1/dataset/create: + post: + consumes: + - application/json + description: 输入数据集名字和URL,创建数据集 + parameters: + - description: 参数描述 + in: body + name: datasetReq + required: true + schema: + $ref: '#/definitions/internal_handler.DatasetReq' + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 创建数据集 + tags: + - Dataset + /v1/dataset/delete/{id}: + delete: + consumes: + - application/json + description: 删除数据集 + parameters: + - in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' "400": description: Request parameter error schema: @@ -6111,14 +7059,14 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 数据集的共享队列 + summary: 删除数据集 tags: - Dataset - /v1/dataset/{datasetId}/queuesNotIn: + /v1/dataset/detail/{datasetId}: get: consumes: - application/json - description: 没有该数据集权限的队列列表 + description: 通过数据集id获取数据集信息 parameters: - in: path name: datasetId @@ -6141,19 +7089,14 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 没有该数据集权限的队列列表 + summary: 通过数据集id获取数据集信息 tags: - Dataset - /v1/dataset/{datasetId}/usersIn: + /v1/dataset/mydataset: get: consumes: - application/json - description: 获取该数据集共享用户 - parameters: - - in: path - name: datasetId - required: true - type: integer + description: 获取数据集 produces: - application/json responses: @@ -6171,26 +7114,28 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 获取该数据集共享用户 + summary: 获取数据集 tags: - Dataset - /v1/dataset/{datasetId}/usersNotIn: - get: + /v1/dataset/share/queue: + post: consumes: - application/json - description: 没有该数据集权限的用户列表 + description: 跟队列共享数据集 parameters: - - in: path - name: datasetId + - description: 共享数据集队列 + in: body + name: queueReq required: true - type: integer + schema: + $ref: '#/definitions/internal_handler.SharedQueueReq' produces: - application/json responses: "200": description: 成功返回值描述 schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' "400": description: Request parameter error schema: @@ -6201,21 +7146,20 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 没有该数据集权限的用户列表 + summary: 跟队列共享数据集 tags: - Dataset - /v1/dataset/cancelshare/queue: + /v1/dataset/share/user: post: consumes: - application/json - description: 普通用户取消数据共享队列 parameters: - - description: 共享数据集队列 + - description: 共享数据集用户 in: body - name: queueReq + name: userReq required: true schema: - $ref: '#/definitions/internal_handler.cancelSharedQueueReq' + $ref: '#/definitions/internal_handler.SharedUserReq' produces: - application/json responses: @@ -6233,21 +7177,21 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 普通用户取消数据共享队列 + summary: 跟用户共享数据集 tags: - Dataset - /v1/dataset/cancelshare/user: + /v1/dataset/update: post: consumes: - application/json - description: 普通用户取消数据集共享 + description: 更新数据集 parameters: - - description: 共享数据集用户 + - description: 参数描述 in: body - name: Req + name: req required: true schema: - $ref: '#/definitions/internal_handler.cancelsharedUserReq' + $ref: '#/definitions/internal_handler.UpdateDatasetreq' produces: - application/json responses: @@ -6265,347 +7209,409 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 普通用户取消数据集共享用户 + summary: 更新数据集 tags: - Dataset - /v1/dataset/create: + /v1/images/account: + get: + consumes: + - application/json + description: 获取未被分享该镜像的账户 + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 获取未被分享该镜像的账户 + tags: + - ImagePack + /v1/images/arch: + post: + consumes: + - application/json + description: 根据镜像ID更新镜像的架构列表 + parameters: + - description: 更新镜像架构信息 + in: body + name: data + required: true + schema: + $ref: '#/definitions/internal_handler_image.UpdateImageArchRequest' + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 更新镜像架构 + tags: + - ImagePack + /v1/images/available: + get: + consumes: + - application/json + description: 用userID & jobType 来过滤已完成的镜像 + parameters: + - enum: + - all + - jupyter + - webide + - pytorch + - tensorflow + - kuberay + - deepspeed + - openmpi + - custom + in: query + name: type + required: true + type: string + x-enum-varnames: + - JobTypeAll + - JobTypeJupyter + - JobTypeWebIDE + - JobTypePytorch + - JobTypeTensorflow + - JobTypeKubeRay + - JobTypeDeepSpeed + - JobTypeOpenMPI + - JobTypeCustom + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 用户在运行作业时选择镜像需要调用此接口,来获取可以用的镜像 + tags: + - ImagePack + /v1/images/change: + post: + consumes: + - application/json + description: 传入uint参数 + parameters: + - description: 更新镜像的ID + in: body + name: req + required: true + schema: + $ref: '#/definitions/internal_handler_image.ChangeImagePublicStatusRequest' + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 管理员模式下更新镜像的公共或私有状态 + tags: + - ImagePack + /v1/images/credential: + post: + consumes: + - application/json + description: 获取参数,生成变量,调用接口 + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 创建用户的harbor项目,并返回用户的harbor项目的凭证 + tags: + - ImagePack + /v1/images/cudabaseimage: + get: + consumes: + - application/json + description: 获取所有可用的Cuda基础镜像列表 + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 获取所有Cuda基础镜像 + tags: + - ImagePack + /v1/images/deleteimage: + post: + consumes: + - application/json + description: 用户模式根据ID列表的ID更新Image的状态为Deleted,起到删除的功能 + parameters: + - description: 删除镜像的ID + in: body + name: ID + required: true + schema: + items: + type: integer + type: array + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 用户模式根据ID列表删除Image + tags: + - ImagePack + /v1/images/description: + post: + consumes: + - application/json + description: 更新描述 + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 更新镜像的描述 + tags: + - ImagePack + /v1/images/dockerfile: + post: + consumes: + - application/json + description: 获取参数,提取Dockerfile中的基础镜像,调用接口 + parameters: + - description: 创建ImagePack CRD + in: body + name: data + required: true + schema: + $ref: '#/definitions/internal_handler_image.CreateByDockerfileRequest' + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 接受用户传入的Dockerfile和描述,创建镜像 + tags: + - ImagePack + /v1/images/envd: post: consumes: - application/json - description: 输入数据集名字和URL,创建数据集 + description: 获取参数,提取Dockerfile中的基础镜像,调用接口 + parameters: + - description: 创建ImagePack CRD + in: body + name: data + required: true + schema: + $ref: '#/definitions/internal_handler_image.CreateByEnvdRequest' + produces: + - application/json + responses: {} + security: + - Bearer: [] + summary: 接受用户传入的Envd内容和描述,创建镜像 + tags: + - ImagePack + /v1/images/get: + get: + consumes: + - application/json + description: 获取imagepackname,搜索到imagepack的模板信息 parameters: - - description: 参数描述 - in: body - name: datasetReq + - description: 获取ImagePack的name + in: query + name: name required: true - schema: - $ref: '#/definitions/internal_handler.DatasetReq' + type: string produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 创建数据集 + summary: 获取imagepack的模板信息 tags: - - Dataset - /v1/dataset/delete/{id}: - delete: + - ImagePack + /v1/images/getbyname: + get: consumes: - application/json - description: 删除数据集 + description: 获取imagepackname,搜索到imagepack parameters: - - in: path - name: id + - description: 获取ImagePack的name + in: query + name: name required: true - type: integer + type: string produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 删除数据集 + summary: 获取imagepack的详细信息 tags: - - Dataset - /v1/dataset/detail/{datasetId}: + - ImagePack + /v1/images/grant: get: consumes: - application/json - description: 通过数据集id获取数据集信息 - parameters: - - in: path - name: datasetId - required: true - type: integer + description: 获取镜像分享到的用户或账户 produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 通过数据集id获取数据集信息 + summary: 获取镜像分享到的用户或账户 tags: - - Dataset - /v1/dataset/mydataset: + - ImagePack + /v1/images/harbor: get: consumes: - application/json - description: 获取数据集 + description: 通过后端获取harbor的部署地址 produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 获取数据集 + summary: 获取harbor的部署地址 tags: - - Dataset - /v1/dataset/share/queue: - post: + - ImagePack + /v1/images/image: + get: consumes: - application/json - description: 跟队列共享数据集 - parameters: - - description: 共享数据集队列 - in: body - name: queueReq - required: true - schema: - $ref: '#/definitions/internal_handler.SharedQueueReq' + description: 返回该用户所有的镜像数据 produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 跟队列共享数据集 + summary: 用户获取所有镜像数据 tags: - - Dataset - /v1/dataset/share/user: + - ImagePack post: consumes: - application/json + description: 获取上传镜像的参数,生成变量,调用接口 parameters: - - description: 共享数据集用户 + - description: 创建Image entity in: body - name: userReq + name: data required: true schema: - $ref: '#/definitions/internal_handler.SharedUserReq' + $ref: '#/definitions/internal_handler_image.UploadImageRequest' produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 跟用户共享数据集 + summary: 用户上传镜像链接 tags: - - Dataset - /v1/dataset/update: - post: + - ImagePack + /v1/images/image/{id}: + delete: consumes: - application/json - description: 更新数据集 + description: 根据ID更新Image的状态为Deleted,起到删除的功能 parameters: - - description: 参数描述 + - description: 删除镜像的ID in: body - name: req + name: ID required: true schema: - $ref: '#/definitions/internal_handler.UpdateDatasetreq' + type: integer produces: - application/json - responses: - "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' - "400": - description: Request parameter error - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' - "500": - description: Other errors - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + responses: {} security: - Bearer: [] - summary: 更新数据集 + summary: 根据ID删除Image tags: - - Dataset - /v1/images/account: + - ImagePack + /v1/images/kaniko: get: consumes: - application/json - description: 获取未被分享该镜像的账户 + description: 返回该用户所有的镜像构建数据 produces: - application/json responses: {} security: - Bearer: [] - summary: 获取未被分享该镜像的账户 + summary: 用户获取镜像构建信息 tags: - ImagePack - /v1/images/arch: post: consumes: - application/json - description: 根据镜像ID更新镜像的架构列表 + description: 获取参数,生成变量,调用接口 parameters: - - description: 更新镜像架构信息 + - description: 创建ImagePack CRD & Kaniko entity in: body name: data required: true schema: - $ref: '#/definitions/internal_handler_image.UpdateImageArchRequest' + $ref: '#/definitions/internal_handler_image.CreateKanikoRequest' produces: - application/json responses: {} security: - Bearer: [] - summary: 更新镜像架构 + summary: 创建ImagePack CRD和数据库Kaniko entity tags: - ImagePack - /v1/images/available: + /v1/images/podname: get: consumes: - application/json - description: 用userID & jobType 来过滤已完成的镜像 - parameters: - - enum: - - all - - jupyter - - webide - - pytorch - - tensorflow - - kuberay - - deepspeed - - openmpi - - custom - in: query - name: type - required: true - type: string - x-enum-varnames: - - JobTypeAll - - JobTypeJupyter - - JobTypeWebIDE - - JobTypePytorch - - JobTypeTensorflow - - JobTypeKubeRay - - JobTypeDeepSpeed - - JobTypeOpenMPI - - JobTypeCustom - produces: - - application/json - responses: {} - security: - - Bearer: [] - summary: 用户在运行作业时选择镜像需要调用此接口,来获取可以用的镜像 - tags: - - ImagePack - /v1/images/change: - post: - consumes: - - application/json - description: 传入uint参数 + description: 根据ID获取镜像构建Pod名称和命名空间 parameters: - - description: 更新镜像的ID - in: body - name: req + - description: 镜像构建任务ID + in: query + name: id required: true - schema: - $ref: '#/definitions/internal_handler_image.ChangeImagePublicStatusRequest' + type: integer produces: - application/json responses: {} security: - Bearer: [] - summary: 管理员模式下更新镜像的公共或私有状态 + summary: 获取镜像构建Pod名称 tags: - ImagePack - /v1/images/credential: - post: + /v1/images/quota: + get: consumes: - application/json - description: 获取参数,生成变量,调用接口 + description: 获取用户的project的详细信息 produces: - application/json responses: {} security: - Bearer: [] - summary: 创建用户的harbor项目,并返回用户的harbor项目的凭证 + summary: 获取用户project的信息 tags: - ImagePack - /v1/images/cudabaseimage: - get: + post: consumes: - application/json - description: 获取所有可用的Cuda基础镜像列表 + description: 传入int64参数,查找用户的project,并更新镜像存储的配额 + parameters: + - description: 更新镜像的ID和存储大小 + in: body + name: req + required: true + schema: + $ref: '#/definitions/internal_handler_image.UpdateProjectQuotaRequest' produces: - application/json responses: {} security: - Bearer: [] - summary: 获取所有Cuda基础镜像 + summary: 更新project的配额 tags: - ImagePack - /v1/images/deleteimage: + /v1/images/remove: post: consumes: - application/json - description: 用户模式根据ID列表的ID更新Image的状态为Deleted,起到删除的功能 + description: 根据任务状态智能处理:若任务状态为 Finished/Failed/Canceled 则执行删除操作;否则执行取消操作 parameters: - - description: 删除镜像的ID + - description: 镜像构建任务ID列表 in: body - name: ID + name: idList required: true schema: items: @@ -6616,441 +7622,711 @@ paths: responses: {} security: - Bearer: [] - summary: 用户模式根据ID列表删除Image + summary: 删除或取消镜像制作任务(批量) tags: - ImagePack - /v1/images/description: - post: + /v1/images/share: + delete: consumes: - application/json - description: 更新描述 + description: 普通用户取消分享镜像到其他账户 produces: - application/json responses: {} security: - Bearer: [] - summary: 更新镜像的描述 + summary: 取消分享镜像到账户 tags: - ImagePack - /v1/images/dockerfile: post: consumes: - application/json - description: 获取参数,提取Dockerfile中的基础镜像,调用接口 - parameters: - - description: 创建ImagePack CRD - in: body - name: data - required: true - schema: - $ref: '#/definitions/internal_handler_image.CreateByDockerfileRequest' + description: 普通用户分享镜像到其他账户或用户 produces: - application/json responses: {} security: - Bearer: [] - summary: 接受用户传入的Dockerfile和描述,创建镜像 + summary: 分享镜像到账户或用户 tags: - ImagePack - /v1/images/envd: + /v1/images/tags: post: consumes: - application/json - description: 获取参数,提取Dockerfile中的基础镜像,调用接口 - parameters: - - description: 创建ImagePack CRD - in: body - name: data - required: true - schema: - $ref: '#/definitions/internal_handler_image.CreateByEnvdRequest' + description: 更新标签 produces: - application/json responses: {} security: - Bearer: [] - summary: 接受用户传入的Envd内容和描述,创建镜像 + summary: 更新镜像的标签 tags: - ImagePack - /v1/images/get: - get: + /v1/images/type: + post: consumes: - application/json - description: 获取imagepackname,搜索到imagepack的模板信息 - parameters: - - description: 获取ImagePack的name - in: query - name: name - required: true - type: string + description: 更新任务类型 produces: - application/json responses: {} security: - Bearer: [] - summary: 获取imagepack的模板信息 + summary: 更新镜像的任务类型 tags: - ImagePack - /v1/images/getbyname: + /v1/images/user: get: consumes: - application/json - description: 获取imagepackname,搜索到imagepack - parameters: - - description: 获取ImagePack的name - in: query - name: name - required: true - type: string + description: 获取未被分享该镜像的用户 produces: - application/json responses: {} security: - Bearer: [] - summary: 获取imagepack的详细信息 + summary: 获取未被分享该镜像的用户(支持名称模糊搜索) tags: - ImagePack - /v1/images/grant: - get: + /v1/images/valid: + post: consumes: - application/json - description: 获取镜像分享到的用户或账户 + description: 通过获取的镜像链接列表,遍历其中的链接,检查是否有效 produces: - application/json responses: {} security: - Bearer: [] - summary: 获取镜像分享到的用户或账户 + summary: 检查镜像链接是否有效 tags: - ImagePack - /v1/images/harbor: + /v1/jobtemplate/{id}: get: consumes: - application/json - description: 通过后端获取harbor的部署地址 + description: 获取作业模板 + parameters: + - description: 作业模板ID + in: path + name: id + required: true + type: integer produces: - application/json - responses: {} + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 获取harbor的部署地址 + summary: 获取作业模板 tags: - - ImagePack - /v1/images/image: - get: + - jobtemplate + /v1/jobtemplate/create: + post: consumes: - application/json - description: 返回该用户所有的镜像数据 + description: 创建作业模板 + parameters: + - description: 作业模板 + in: body + name: req + required: true + schema: + $ref: '#/definitions/internal_handler.JobTemplateReq' produces: - application/json - responses: {} + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 用户获取所有镜像数据 + summary: 创建作业模板 tags: - - ImagePack - post: + - jobtemplate + /v1/jobtemplate/delete/{id}: + delete: consumes: - application/json - description: 获取上传镜像的参数,生成变量,调用接口 + description: 删除作业模板 parameters: - - description: 创建Image entity - in: body - name: data + - description: 作业模板ID + in: path + name: id required: true - schema: - $ref: '#/definitions/internal_handler_image.UploadImageRequest' + type: integer produces: - application/json - responses: {} + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 用户上传镜像链接 + summary: 删除作业模板 tags: - - ImagePack - /v1/images/image/{id}: - delete: + - jobtemplate + /v1/jobtemplate/list: + get: consumes: - application/json - description: 根据ID更新Image的状态为Deleted,起到删除的功能 + description: 展示所有作业模板 + produces: + - application/json + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: 展示所有作业模板 + tags: + - jobtemplate + /v1/jobtemplate/update: + put: + consumes: + - application/json + description: 更新作业模板 parameters: - - description: 删除镜像的ID + - description: 作业模板 in: body - name: ID + name: req required: true schema: - type: integer + $ref: '#/definitions/internal_handler.UpdateJobTemplateReq' produces: - application/json - responses: {} + responses: + "200": + description: 成功返回值描述 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 根据ID删除Image + summary: 更新作业模板 tags: - - ImagePack - /v1/images/kaniko: + - jobtemplate + /v1/kthena/inference-services: get: consumes: - application/json - description: 返回该用户所有的镜像构建数据 + description: List Kthena inference services owned by the current user and account. produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaServiceResp' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 用户获取镜像构建信息 + summary: List my inference services tags: - - ImagePack + - kthena post: consumes: - application/json - description: 获取参数,生成变量,调用接口 + description: Create a Kthena ModelBooster-backed inference service. parameters: - - description: 创建ImagePack CRD & Kaniko entity + - description: Create inference service request in: body - name: data + name: request required: true schema: - $ref: '#/definitions/internal_handler_image.CreateKanikoRequest' + $ref: '#/definitions/internal_handler.CreateKthenaReq' produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 创建ImagePack CRD和数据库Kaniko entity + summary: Create inference service tags: - - ImagePack - /v1/images/podname: - get: + - kthena + /v1/kthena/inference-services/{name}: + delete: consumes: - application/json - description: 根据ID获取镜像构建Pod名称和命名空间 + description: Delete a Kthena inference service owned by the current user and + account. parameters: - - description: 镜像构建任务ID - in: query - name: id + - description: Inference service name + in: path + name: name required: true - type: integer + type: string produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 获取镜像构建Pod名称 + summary: Delete inference service tags: - - ImagePack - /v1/images/quota: + - kthena get: consumes: - application/json - description: 获取用户的project的详细信息 + description: Get a Kthena inference service owned by the current user and account. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaServiceResp' + "400": + description: Request parameter error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Other errors + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 获取用户project的信息 + summary: Get inference service tags: - - ImagePack - post: - consumes: - - application/json - description: 传入int64参数,查找用户的project,并更新镜像存储的配额 + - kthena + /v1/kthena/inference-services/{name}/conversations: + get: + description: List a user's deployment-scoped conversations; messages are omitted + by default. parameters: - - description: 更新镜像的ID和存储大小 - in: body - name: req + - description: Inference service name + in: path + name: name required: true - schema: - $ref: '#/definitions/internal_handler_image.UpdateProjectQuotaRequest' + type: string + - description: Include recent messages + in: query + name: includeMessages + type: boolean + - description: Conversation limit, maximum 100 + in: query + name: limit + type: integer + - description: Messages per conversation, maximum 500 + in: query + name: messageLimit + type: integer produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaConversationResp' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 更新project的配额 + summary: List model deployment conversations tags: - - ImagePack - /v1/images/remove: + - kthena post: consumes: - application/json - description: 根据任务状态智能处理:若任务状态为 Finished/Failed/Canceled 则执行删除操作;否则执行取消操作 + description: Create a user/deployment-scoped conversation; empty sessionId gets + a UUID and supplied UUIDs are idempotent. parameters: - - description: 镜像构建任务ID列表 + - description: Inference service name + in: path + name: name + required: true + type: string + - description: Conversation in: body - name: idList + name: request required: true schema: - items: - type: integer - type: array + $ref: '#/definitions/internal_handler.KthenaConversationCreateReq' produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 删除或取消镜像制作任务(批量) + summary: Create model deployment conversation tags: - - ImagePack - /v1/images/share: + - kthena + /v1/kthena/inference-services/{name}/conversations/{sessionId}: delete: - consumes: - - application/json - description: 普通用户取消分享镜像到其他账户 - produces: - - application/json - responses: {} - security: - - Bearer: [] - summary: 取消分享镜像到账户 - tags: - - ImagePack - post: - consumes: - - application/json - description: 普通用户分享镜像到其他账户或用户 - produces: - - application/json - responses: {} - security: - - Bearer: [] - summary: 分享镜像到账户或用户 - tags: - - ImagePack - /v1/images/tags: - post: - consumes: - - application/json - description: 更新标签 + description: Permanently delete one current user's persisted Kthena conversation + and all of its messages. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + - description: Conversation session UUID + in: path + name: sessionId + required: true + type: string produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 更新镜像的标签 + summary: Delete model deployment conversation tags: - - ImagePack - /v1/images/type: - post: - consumes: - - application/json - description: 更新任务类型 + - kthena + get: + description: Get one persisted conversation and its most recent ordered messages + for the current user and authorized Kthena deployment. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + - description: Conversation session UUID + in: path + name: sessionId + required: true + type: string + - description: Maximum recent messages, maximum 500 + in: query + name: messageLimit + type: integer produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 更新镜像的任务类型 + summary: Get model deployment conversation tags: - - ImagePack - /v1/images/user: - get: + - kthena + patch: consumes: - application/json - description: 获取未被分享该镜像的用户 + description: 'Update title and/or replace all messages of a current user''s + conversation. Sending messages: [] clears the message list.' + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + - description: Conversation session UUID + in: path + name: sessionId + required: true + type: string + - description: Conversation update + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.KthenaConversationUpdateReq' produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationResp' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 获取未被分享该镜像的用户(支持名称模糊搜索) + summary: Update model deployment conversation tags: - - ImagePack - /v1/images/valid: + - kthena + /v1/kthena/inference-services/{name}/conversations/{sessionId}/turns: post: consumes: - application/json - description: 通过获取的镜像链接列表,遍历其中的链接,检查是否有效 + description: Use stored context to call Kthena and atomically save successful + user and assistant messages. + parameters: + - description: Inference service name + in: path + name: name + required: true + type: string + - description: Conversation session UUID + in: path + name: sessionId + required: true + type: string + - description: User turn + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.KthenaConversationTurnReq' produces: - application/json - responses: {} + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "502": + description: Bad Gateway + schema: {} security: - Bearer: [] - summary: 检查镜像链接是否有效 + summary: Send a persisted model deployment conversation turn tags: - - ImagePack - /v1/jobtemplate/{id}: - get: + - kthena + /v1/kthena/inference-services/{name}/conversations/turns: + post: consumes: - application/json - description: 获取作业模板 + description: Send an atomic turn without a path sessionId. Provide a body sessionId + to reuse a UUID, or leave it empty for a new UUID. parameters: - - description: 作业模板ID + - description: Inference service name in: path - name: id + name: name required: true - type: integer + type: string + - description: User turn + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.KthenaConversationTurnReq' produces: - application/json responses: "200": - description: 成功返回值描述 + description: OK schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaConversationTurnResp' "400": - description: Request parameter error + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Not Found schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' "500": - description: Other errors + description: Internal Server Error schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "502": + description: Bad Gateway + schema: {} security: - Bearer: [] - summary: 获取作业模板 + summary: Send a new or existing persisted model deployment conversation turn tags: - - jobtemplate - /v1/jobtemplate/create: + - kthena + /v1/kthena/inference-services/{name}/openai/{path}: post: consumes: - application/json - description: 创建作业模板 + description: Proxy an OpenAI-compatible request to kthena-router for an inference + service owned by the current user and account. parameters: - - description: 作业模板 - in: body - name: req + - description: Inference service name + in: path + name: name required: true + type: string + - description: OpenAI-compatible API path + in: path + name: path + required: true + type: string + - description: OpenAI-compatible request body + in: body + name: request schema: - $ref: '#/definitions/internal_handler.JobTemplateReq' + $ref: '#/definitions/internal_handler.KthenaProxyReq' produces: - application/json responses: "200": - description: 成功返回值描述 - schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + description: OK + schema: {} "400": description: Request parameter error schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Model route or runtime pod not found + schema: {} "500": description: Other errors schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 创建作业模板 + summary: Proxy OpenAI-compatible inference request tags: - - jobtemplate - /v1/jobtemplate/delete/{id}: - delete: + - kthena + /v1/kthena/inference-services/{name}/yaml: + get: consumes: - application/json - description: 删除作业模板 + description: Get raw Kthena ModelBooster object for an inference service owned + by the current user and account. parameters: - - description: 作业模板ID + - description: Inference service name in: path - name: id + name: name required: true - type: integer + type: string produces: - application/json responses: "200": - description: 成功返回值描述 + description: OK schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' "400": description: Request parameter error schema: @@ -7061,66 +8337,134 @@ paths: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 删除作业模板 + summary: Get inference service YAML tags: - - jobtemplate - /v1/jobtemplate/list: + - kthena + /v1/kthena/inference-templates: get: + description: List the current user's templates in the active account. Templates + are never shared across users or accounts. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-array_internal_handler_KthenaInferenceTemplateResp' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: List private Kthena deployment templates + tags: + - kthena + post: consumes: - application/json - description: 展示所有作业模板 + description: Save the current deployment form as a private template for the + current user and account. + parameters: + - description: Template + in: body + name: request + required: true + schema: + $ref: '#/definitions/internal_handler.KthenaInferenceTemplateReq' produces: - application/json responses: "200": - description: 成功返回值描述 + description: OK schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp' "400": - description: Request parameter error + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "409": + description: Conflict schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' "500": - description: Other errors + description: Internal Server Error schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 展示所有作业模板 + summary: Create a private Kthena deployment template tags: - - jobtemplate - /v1/jobtemplate/update: + - kthena + /v1/kthena/inference-templates/{id}: + delete: + description: Delete one template owned by the current user in the active account. + parameters: + - description: Template ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + security: + - Bearer: [] + summary: Delete a private Kthena deployment template + tags: + - kthena put: consumes: - application/json - description: 更新作业模板 + description: Replace a template owned by the current user in the active account. parameters: - - description: 作业模板 + - description: Template ID + in: path + name: id + required: true + type: integer + - description: Template in: body - name: req + name: request required: true schema: - $ref: '#/definitions/internal_handler.UpdateJobTemplateReq' + $ref: '#/definitions/internal_handler.KthenaInferenceTemplateReq' produces: - application/json responses: "200": - description: 成功返回值描述 + description: OK schema: - $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-string' + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceTemplateResp' "400": - description: Request parameter error + description: Bad Request + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' + "404": + description: Not Found schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' "500": - description: Other errors + description: Internal Server Error schema: $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-any' security: - Bearer: [] - summary: 更新作业模板 + summary: Update a private Kthena deployment template tags: - - jobtemplate + - kthena /v1/models/download: post: consumes: @@ -8457,6 +9801,21 @@ paths: summary: 获取资源统计信息 tags: - statistics + /v1/system-config/kthena-inference: + get: + description: 查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。 + produces: + - application/json + responses: + "200": + description: 开关状态 + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-internal_handler_KthenaInferenceStatusResp' + security: + - Bearer: [] + summary: 获取模型部署功能开关状态 + tags: + - SystemConfig /v1/system-config/model-download-limit: get: description: 获取当前用户的并发任务上限、滚动窗口成功下载上限和白名单豁免状态 @@ -9428,6 +10787,89 @@ paths: summary: Create a WebIDE job tags: - VolcanoJob + /v1/vcjobs/workloads: + get: + description: Lists persisted Volcano jobs and current-user Kthena ModelBoosters + as a single pageable list. + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size, 1-200 + in: query + name: page_size + type: integer + - description: Sort fields + in: query + name: sort + type: string + - description: Search workloads + in: query + name: search + type: string + - description: Number of days to look back, -1 for all + in: query + name: days + type: integer + - collectionFormat: multi + description: Job types, including model-deployment + in: query + items: + type: string + name: job_type + type: array + - collectionFormat: multi + description: Workload kinds + in: query + items: + type: string + name: workload_kind + type: array + - collectionFormat: multi + description: Schedule types + in: query + items: + type: integer + name: schedule_type + type: array + - collectionFormat: multi + description: Workload statuses + in: query + items: + type: string + name: status + type: array + - description: Node name + in: query + name: node + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_Page-internal_handler_vcjob_WorkloadResp' + security: + - Bearer: [] + summary: Get current user's unified workloads + tags: + - VolcanoJob + /v1/vcjobs/workloads/facets: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_raids-lab_crater_internal_resputil.Response-github_com_raids-lab_crater_internal_resputil_FacetResponse' + security: + - Bearer: [] + summary: Get current user's unified workload facets + tags: + - VolcanoJob securityDefinitions: Bearer: description: 访问 /login 并获取 TOKEN 后,填入 'Bearer ${TOKEN}' 以访问受保护的接口 diff --git a/backend/internal/handler/inference_conversation.go b/backend/internal/handler/inference_conversation.go new file mode 100644 index 000000000..46cf0fe2c --- /dev/null +++ b/backend/internal/handler/inference_conversation.go @@ -0,0 +1,1147 @@ +package handler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "sort" + "strings" + "time" + "unicode/utf8" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/datatypes" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/klog/v2" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/internal/bizerr" + "github.com/raids-lab/crater/internal/resputil" + "github.com/raids-lab/crater/internal/util" +) + +const ( + defaultKthenaConversationLimit = 30 + maxKthenaConversationLimit = 100 + defaultKthenaConversationMessageLimit = 100 + maxKthenaConversationMessageLimit = 500 + maxKthenaConversationMessages = 500 + maxKthenaConversationTitleRunes = 256 + maxKthenaConversationContentRunes = 32768 + maxKthenaConversationTurnHistory = 100 + maxKthenaConversationSessionIDRunes = 128 + kthenaConversationTitlePreviewRunes = 48 + kthenaConversationLogVerbosity = 4 +) + +const ( + kthenaConversationRoleSystem = "system" + kthenaConversationRoleUser = "user" + kthenaConversationRoleAssistant = "assistant" +) + +type kthenaConversationStore struct { + db *gorm.DB +} + +func newKthenaConversationStore(db *gorm.DB) *kthenaConversationStore { + return &kthenaConversationStore{db: db} +} + +// KthenaConversationMessageReq is one OpenAI-compatible message persisted in +// a conversation. Update requests replace the complete ordered message list. +type KthenaConversationMessageReq struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// KthenaConversationCreateReq creates a conversation. SessionID is optional: +// existing clients can provide their own UUID, while an empty value gets a +// server-generated UUID in the response. +type KthenaConversationCreateReq struct { + SessionID string `json:"sessionId"` + Title string `json:"title"` + Messages []KthenaConversationMessageReq `json:"messages"` +} + +// KthenaConversationUpdateReq changes the title and/or replaces all messages. +// A nil Messages field means leave the current messages untouched; an empty +// array clears them. +type KthenaConversationUpdateReq struct { + Title *string `json:"title"` + Messages *[]KthenaConversationMessageReq `json:"messages"` +} + +// KthenaConversationListReq controls the bounded conversation history list. +type KthenaConversationListReq struct { + IncludeMessages bool `form:"includeMessages"` + Limit int `form:"limit"` + MessageLimit int `form:"messageLimit"` +} + +type kthenaConversationGetReq struct { + MessageLimit int `form:"messageLimit"` +} + +// KthenaConversationTurnReq sends one user turn atomically. The backend reads +// the persisted context, calls Kthena, and stores the user/assistant pair only +// after a successful non-streaming completion. ClientTurnID is optional but +// makes retries idempotent for an existing conversation. +type KthenaConversationTurnReq struct { + SessionID string `json:"sessionId"` + Content string `json:"content"` + Temperature *float64 `json:"temperature"` + MaxTokens *int64 `json:"maxTokens"` + ClientTurnID string `json:"clientTurnId"` +} + +// KthenaConversationMessageResp is a stored message returned to the client. +type KthenaConversationMessageResp struct { + Sequence int `json:"sequence"` + Role string `json:"role"` + Content string `json:"content"` + CreatedAt time.Time `json:"createdAt"` +} + +// KthenaConversationResp is the persisted, deployment-scoped conversation. +// Messages are populated for conversation detail and when requested from list. +type KthenaConversationResp struct { + SessionID string `json:"sessionId"` + Title string `json:"title"` + Namespace string `json:"namespace"` + ServiceName string `json:"serviceName"` + ModelName string `json:"modelName"` + BackendType string `json:"backendType"` + MessageCount int `json:"messageCount"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Messages []KthenaConversationMessageResp `json:"messages,omitempty"` +} + +// KthenaConversationTurnResp returns the canonical persisted assistant turn +// alongside the original OpenAI-compatible completion object. +type KthenaConversationTurnResp struct { + Conversation KthenaConversationResp `json:"conversation"` + Assistant KthenaConversationMessageResp `json:"assistant"` + Completion json.RawMessage `json:"completion" swaggertype:"object"` +} + +type kthenaConversationScope struct { + UserID uint + AccountID uint + Username string + Namespace string + ServiceName string + // ModelName is the served model snapshot persisted with the conversation. + // It is intentionally distinct from RouteModelName: Kthena routes requests + // by the ModelBooster/ModelRoute name, while a vLLM served-model-name can be + // an entirely different user-facing identifier. + ModelName string + RouteModelName string + BackendType string +} + +// ListKthenaConversations godoc +// +// @Summary List model deployment conversations +// @Description List a user's deployment-scoped conversations; messages are omitted by default. +// @Tags kthena +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param includeMessages query bool false "Include recent messages" +// @Param limit query int false "Conversation limit, maximum 100" +// @Param messageLimit query int false "Messages per conversation, maximum 500" +// @Success 200 {object} resputil.Response[[]KthenaConversationResp] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-services/{name}/conversations [get] +func (mgr *KthenaMgr) ListKthenaConversations(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + + var req KthenaConversationListReq + if err := c.ShouldBindQuery(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid conversation list query")) + return + } + req.Limit = normalizeKthenaConversationLimit( + req.Limit, defaultKthenaConversationLimit, maxKthenaConversationLimit, + ) + req.MessageLimit = normalizeKthenaConversationLimit( + req.MessageLimit, defaultKthenaConversationMessageLimit, maxKthenaConversationMessageLimit, + ) + + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + conversations, err := store.list(c.Request.Context(), &scope, req.Limit) + if err != nil { + kthenaConversationDatabaseError(c, err, "list conversations failed") + return + } + + response := make([]KthenaConversationResp, 0, len(conversations)) + for index := range conversations { + var messages []model.KthenaChatMessage + if req.IncludeMessages { + messages, err = store.messages(c.Request.Context(), conversations[index].ID, req.MessageLimit) + if err != nil { + kthenaConversationDatabaseError(c, err, "list conversation messages failed") + return + } + } + response = append(response, kthenaConversationToResp(&conversations[index], messages)) + } + resputil.Success(c, response) +} + +// CreateKthenaConversation godoc +// +// @Summary Create model deployment conversation +// @Description Create a user/deployment-scoped conversation; empty sessionId gets a UUID and supplied UUIDs are idempotent. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param request body KthenaConversationCreateReq true "Conversation" +// @Success 200 {object} resputil.Response[KthenaConversationResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-services/{name}/conversations [post] +func (mgr *KthenaMgr) CreateKthenaConversation(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + var req KthenaConversationCreateReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid conversation request")) + return + } + if err := validateKthenaConversationCreate(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + + conversation, existed, err := store.create( + c.Request.Context(), &scope, req.SessionID, req.Title, req.Messages, + ) + if err != nil { + kthenaConversationDatabaseError(c, err, "create conversation failed") + return + } + messages, err := store.messages(c.Request.Context(), conversation.ID, maxKthenaConversationMessageLimit) + if err != nil { + kthenaConversationDatabaseError(c, err, "load conversation messages failed") + return + } + if existed { + klog.V(kthenaConversationLogVerbosity).Infof( + "Kthena conversation create reused client session %q", conversation.ClientSessionID, + ) + } + resputil.Success(c, kthenaConversationToResp(&conversation, messages)) +} + +// GetKthenaConversation godoc +// +// @Summary Get model deployment conversation +// @Description Get one persisted conversation and its most recent ordered messages for the current user and authorized Kthena deployment. +// @Tags kthena +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param sessionId path string true "Conversation session UUID" +// @Param messageLimit query int false "Maximum recent messages, maximum 500" +// @Success 200 {object} resputil.Response[KthenaConversationResp] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-services/{name}/conversations/{sessionId} [get] +func (mgr *KthenaMgr) GetKthenaConversation(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + sessionID, ok := kthenaConversationSessionID(c) + if !ok { + return + } + var req kthenaConversationGetReq + if err := c.ShouldBindQuery(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid conversation query")) + return + } + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + conversation, err := store.find(c.Request.Context(), &scope, sessionID) + if err != nil { + kthenaConversationFindError(c, err) + return + } + messages, err := store.messages( + c.Request.Context(), conversation.ID, + normalizeKthenaConversationLimit( + req.MessageLimit, defaultKthenaConversationMessageLimit, maxKthenaConversationMessageLimit, + ), + ) + if err != nil { + kthenaConversationDatabaseError(c, err, "load conversation messages failed") + return + } + resputil.Success(c, kthenaConversationToResp(&conversation, messages)) +} + +// UpdateKthenaConversation godoc +// +// @Summary Update model deployment conversation +// @Description Update title and/or replace all messages of a current user's conversation. Sending messages: [] clears the message list. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param sessionId path string true "Conversation session UUID" +// @Param request body KthenaConversationUpdateReq true "Conversation update" +// @Success 200 {object} resputil.Response[KthenaConversationResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-services/{name}/conversations/{sessionId} [patch] +func (mgr *KthenaMgr) UpdateKthenaConversation(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + sessionID, ok := kthenaConversationSessionID(c) + if !ok { + return + } + var req KthenaConversationUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid conversation update")) + return + } + if err := validateKthenaConversationUpdate(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + conversation, err := store.update(c.Request.Context(), &scope, sessionID, req.Title, req.Messages) + if err != nil { + kthenaConversationFindOrDatabaseError(c, err, "update conversation failed") + return + } + messages, err := store.messages(c.Request.Context(), conversation.ID, maxKthenaConversationMessageLimit) + if err != nil { + kthenaConversationDatabaseError(c, err, "load conversation messages failed") + return + } + resputil.Success(c, kthenaConversationToResp(&conversation, messages)) +} + +// DeleteKthenaConversation godoc +// +// @Summary Delete model deployment conversation +// @Description Permanently delete one current user's persisted Kthena conversation and all of its messages. +// @Tags kthena +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param sessionId path string true "Conversation session UUID" +// @Success 200 {object} resputil.Response[string] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-services/{name}/conversations/{sessionId} [delete] +func (mgr *KthenaMgr) DeleteKthenaConversation(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + sessionID, ok := kthenaConversationSessionID(c) + if !ok { + return + } + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + if err := store.delete(c.Request.Context(), &scope, sessionID); err != nil { + kthenaConversationFindOrDatabaseError(c, err, "delete conversation failed") + return + } + resputil.Success(c, "conversation deleted") +} + +// CreateKthenaConversationTurn godoc +// +// @Summary Send a persisted model deployment conversation turn +// @Description Use stored context to call Kthena and atomically save successful user and assistant messages. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param sessionId path string true "Conversation session UUID" +// @Param request body KthenaConversationTurnReq true "User turn" +// @Success 200 {object} resputil.Response[KthenaConversationTurnResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Failure 502 {object} any +// @Router /v1/kthena/inference-services/{name}/conversations/{sessionId}/turns [post] +// +//nolint:gocyclo // The handler keeps request validation, proxy failure passthrough, and atomic persistence visible at the API boundary. +func (mgr *KthenaMgr) CreateKthenaConversationTurn(c *gin.Context) { + scope, ok := mgr.loadKthenaConversationScope(c) + if !ok { + return + } + var req KthenaConversationTurnReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid conversation turn")) + return + } + if err := validateKthenaConversationTurn(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + sessionID := strings.TrimSpace(c.Param("sessionID")) + if sessionID != "" && req.SessionID != "" && sessionID != req.SessionID { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.New("sessionId path and request body differ")) + return + } + if sessionID == "" { + sessionID = req.SessionID + } + if err := validateKthenaConversationSessionID(sessionID); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + + store, ok := mgr.kthenaConversationStore(c) + if !ok { + return + } + conversation, _, err := store.create(c.Request.Context(), &scope, sessionID, "", nil) + if err != nil { + kthenaConversationDatabaseError(c, err, "prepare conversation failed") + return + } + + if req.ClientTurnID != "" { + userMessage, assistantMessage, found, findErr := store.findTurn( + c.Request.Context(), conversation.ID, req.ClientTurnID, + ) + if findErr != nil { + kthenaConversationDatabaseError(c, findErr, "find prior conversation turn failed") + return + } + if found { + _ = userMessage + mgr.respondKthenaConversationTurn(c, store, &conversation, &assistantMessage) + return + } + } + + history, err := store.messages(c.Request.Context(), conversation.ID, maxKthenaConversationTurnHistory) + if err != nil { + kthenaConversationDatabaseError(c, err, "load conversation context failed") + return + } + body, err := buildKthenaConversationTurnBody(scope.RouteModelName, history, req) + if err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + rawCompletion, err := mgr.proxyKthenaRouter( + c.Request.Context(), http.MethodPost, "v1/chat/completions", body, c.Request.Header, + ) + if err != nil { + klog.Errorf("proxy persisted inference conversation turn failed: %v", err) + if len(rawCompletion) > 0 { + c.Data(kthenaProxyHTTPStatus(err), "application/json", rawCompletion) + return + } + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "proxy inference request failed")) + return + } + assistant, err := kthenaAssistantMessageFromCompletion(rawCompletion) + if err != nil { + resputil.HandleError(c, bizerr.Internal.ServiceError.Wrap(err, "invalid inference completion response")) + return + } + + conversation, assistantMessage, alreadySaved, err := store.appendTurn( + c.Request.Context(), &scope, conversation.ClientSessionID, req, assistant, rawCompletion, + ) + if err != nil { + kthenaConversationFindOrDatabaseError(c, err, "persist conversation turn failed") + return + } + if alreadySaved { + klog.V(kthenaConversationLogVerbosity).Infof( + "Kthena conversation turn %q was concurrently persisted", req.ClientTurnID, + ) + } + mgr.respondKthenaConversationTurn(c, store, &conversation, &assistantMessage) +} + +// CreateKthenaConversationTurnWithoutSession godoc +// +// @Summary Send a new or existing persisted model deployment conversation turn +// @Description Send an atomic turn without a path sessionId. Provide a body sessionId to reuse a UUID, or leave it empty for a new UUID. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param request body KthenaConversationTurnReq true "User turn" +// @Success 200 {object} resputil.Response[KthenaConversationTurnResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Failure 502 {object} any +// @Router /v1/kthena/inference-services/{name}/conversations/turns [post] +func (mgr *KthenaMgr) CreateKthenaConversationTurnWithoutSession(c *gin.Context) { + mgr.CreateKthenaConversationTurn(c) +} + +func (mgr *KthenaMgr) respondKthenaConversationTurn( + c *gin.Context, + store *kthenaConversationStore, + conversation *model.KthenaChatSession, + assistant *model.KthenaChatMessage, +) { + messages, err := store.messages(c.Request.Context(), conversation.ID, maxKthenaConversationMessageLimit) + if err != nil { + kthenaConversationDatabaseError(c, err, "load persisted conversation failed") + return + } + completion := json.RawMessage(assistant.ResponseJSON) + if len(completion) == 0 || !json.Valid(completion) { + completion = nil + } + resputil.Success(c, KthenaConversationTurnResp{ + Conversation: kthenaConversationToResp(conversation, messages), + Assistant: kthenaConversationMessageToResp(assistant), + Completion: completion, + }) +} + +func (mgr *KthenaMgr) loadKthenaConversationScope( + c *gin.Context, +) (kthenaConversationScope, bool) { + obj, ok := mgr.loadKthenaService(c, false) + if !ok { + return kthenaConversationScope{}, false + } + scope, err := kthenaConversationScopeFromModelBooster(c, obj) + if err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return kthenaConversationScope{}, false + } + return scope, true +} + +func (mgr *KthenaMgr) kthenaConversationStore(c *gin.Context) (*kthenaConversationStore, bool) { + if mgr.conversationStore == nil || mgr.conversationStore.db == nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.New("Kthena conversation storage is not initialized")) + return nil, false + } + return mgr.conversationStore, true +} + +func kthenaConversationScopeFromModelBooster( + c *gin.Context, obj *unstructured.Unstructured, +) (kthenaConversationScope, error) { + if obj == nil { + return kthenaConversationScope{}, bizerr.BadRequest.ParameterError.New("inference service is required") + } + token := util.GetToken(c) + if token.UserID == 0 { + return kthenaConversationScope{}, bizerr.BadRequest.ParameterError.New("current user is required") + } + modelName := strings.TrimSpace(servedModelFromModelBooster(obj)) + if modelName == "" { + modelName = obj.GetName() + } + routeModelName := strings.TrimSpace(obj.GetName()) + if routeModelName == "" { + routeModelName = modelName + } + backend, _, _ := unstructured.NestedMap(obj.Object, "spec", "backend") + return kthenaConversationScope{ + UserID: token.UserID, + AccountID: token.AccountID, + Username: strings.TrimSpace(token.Username), + Namespace: obj.GetNamespace(), + ServiceName: obj.GetName(), + ModelName: modelName, + RouteModelName: routeModelName, + BackendType: strings.TrimSpace(stringValue(backend[kthenaSpecTypeKey])), + }, nil +} + +func kthenaConversationSessionID(c *gin.Context) (string, bool) { + sessionID := strings.TrimSpace(c.Param("sessionID")) + if err := validateKthenaConversationSessionID(sessionID); err != nil || sessionID == "" { + if err == nil { + err = bizerr.BadRequest.ParameterError.New("sessionId is required") + } + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return "", false + } + return sessionID, true +} + +func validateKthenaConversationCreate(req *KthenaConversationCreateReq) error { + if req == nil { + return bizerr.BadRequest.ParameterError.New("conversation request is required") + } + req.SessionID = strings.TrimSpace(req.SessionID) + req.Title = strings.TrimSpace(req.Title) + if err := validateKthenaConversationSessionID(req.SessionID); err != nil { + return err + } + if err := validateKthenaConversationTitle(req.Title); err != nil { + return err + } + messages, err := normalizeKthenaConversationMessages(req.Messages) + if err != nil { + return err + } + req.Messages = messages + return nil +} + +func validateKthenaConversationUpdate(req *KthenaConversationUpdateReq) error { + if req == nil || (req.Title == nil && req.Messages == nil) { + return bizerr.BadRequest.ParameterError.New("title or messages is required") + } + if req.Title != nil { + title := strings.TrimSpace(*req.Title) + if err := validateKthenaConversationTitle(title); err != nil { + return err + } + req.Title = &title + } + if req.Messages != nil { + messages, err := normalizeKthenaConversationMessages(*req.Messages) + if err != nil { + return err + } + req.Messages = &messages + } + return nil +} + +func validateKthenaConversationTurn(req *KthenaConversationTurnReq) error { + if req == nil { + return bizerr.BadRequest.ParameterError.New("conversation turn is required") + } + req.SessionID = strings.TrimSpace(req.SessionID) + req.ClientTurnID = strings.TrimSpace(req.ClientTurnID) + req.Content = strings.TrimSpace(req.Content) + if err := validateKthenaConversationSessionID(req.SessionID); err != nil { + return err + } + if err := validateKthenaConversationSessionID(req.ClientTurnID); err != nil { + return bizerr.BadRequest.ParameterError.Wrap(err, "invalid clientTurnId") + } + if req.Content == "" { + return bizerr.BadRequest.ParameterError.New("content is required") + } + if utf8.RuneCountInString(req.Content) > maxKthenaConversationContentRunes { + return bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("content must not exceed %d characters", maxKthenaConversationContentRunes), + ) + } + if req.Temperature != nil && (*req.Temperature < 0 || *req.Temperature > 2) { + return bizerr.BadRequest.ParameterError.New("temperature must be between 0 and 2") + } + if req.MaxTokens != nil && *req.MaxTokens <= 0 { + return bizerr.BadRequest.ParameterError.New("maxTokens must be greater than 0") + } + return nil +} + +func validateKthenaConversationSessionID(value string) error { + if utf8.RuneCountInString(value) > maxKthenaConversationSessionIDRunes { + return bizerr.BadRequest.ParameterError.New("sessionId must not exceed 128 characters") + } + return nil +} + +func validateKthenaConversationTitle(value string) error { + if utf8.RuneCountInString(value) > maxKthenaConversationTitleRunes { + return bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("title must not exceed %d characters", maxKthenaConversationTitleRunes), + ) + } + return nil +} + +func normalizeKthenaConversationMessages( + messages []KthenaConversationMessageReq, +) ([]KthenaConversationMessageReq, error) { + if len(messages) > maxKthenaConversationMessages { + return nil, bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("messages must not exceed %d entries", maxKthenaConversationMessages), + ) + } + normalized := make([]KthenaConversationMessageReq, len(messages)) + for index, message := range messages { + role := strings.ToLower(strings.TrimSpace(message.Role)) + switch role { + case kthenaConversationRoleSystem, kthenaConversationRoleUser, kthenaConversationRoleAssistant: + default: + return nil, bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("messages[%d].role must be system, user, or assistant", index), + ) + } + content := strings.TrimSpace(message.Content) + if role != kthenaConversationRoleAssistant && content == "" { + return nil, bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("messages[%d].content is required", index), + ) + } + if utf8.RuneCountInString(content) > maxKthenaConversationContentRunes { + return nil, bizerr.BadRequest.ParameterError.New( + fmt.Sprintf( + "messages[%d].content must not exceed %d characters", index, maxKthenaConversationContentRunes, + ), + ) + } + normalized[index] = KthenaConversationMessageReq{Role: role, Content: content} + } + return normalized, nil +} + +func normalizeKthenaConversationLimit(value, fallback, maximum int) int { + if value <= 0 { + return fallback + } + if value > maximum { + return maximum + } + return value +} + +func kthenaConversationTitle(messages []KthenaConversationMessageReq) string { + for _, message := range messages { + if message.Role != kthenaConversationRoleUser || message.Content == "" { + continue + } + title := strings.Join(strings.Fields(message.Content), " ") + runes := []rune(title) + if len(runes) > kthenaConversationTitlePreviewRunes { + return string(runes[:kthenaConversationTitlePreviewRunes]) + "…" + } + return title + } + return "" +} + +func kthenaConversationToResp( + conversation *model.KthenaChatSession, messages []model.KthenaChatMessage, +) KthenaConversationResp { + response := KthenaConversationResp{ + SessionID: conversation.ClientSessionID, + Title: conversation.Title, + Namespace: conversation.Namespace, + ServiceName: conversation.ServiceName, + ModelName: conversation.ModelName, + BackendType: conversation.BackendType, + MessageCount: conversation.MessageCount, + CreatedAt: conversation.CreatedAt, + UpdatedAt: conversation.UpdatedAt, + } + if messages != nil { + response.Messages = make([]KthenaConversationMessageResp, 0, len(messages)) + for index := range messages { + response.Messages = append(response.Messages, kthenaConversationMessageToResp(&messages[index])) + } + } + return response +} + +func kthenaConversationMessageToResp(message *model.KthenaChatMessage) KthenaConversationMessageResp { + return KthenaConversationMessageResp{ + Sequence: message.Sequence, + Role: message.Role, + Content: message.Content, + CreatedAt: message.CreatedAt, + } +} + +func (store *kthenaConversationStore) list( + ctx context.Context, scope *kthenaConversationScope, limit int, +) ([]model.KthenaChatSession, error) { + conversations := make([]model.KthenaChatSession, 0) + err := store.scoped(store.db.WithContext(ctx), scope). + Order("updated_at DESC, id DESC"). + Limit(limit). + Find(&conversations).Error + return conversations, err +} + +func (store *kthenaConversationStore) find( + ctx context.Context, scope *kthenaConversationScope, clientSessionID string, +) (model.KthenaChatSession, error) { + var conversation model.KthenaChatSession + err := store.scoped(store.db.WithContext(ctx), scope). + Where("client_session_id = ?", clientSessionID). + First(&conversation).Error + return conversation, err +} + +func (store *kthenaConversationStore) messages( + ctx context.Context, sessionID uint, limit int, +) ([]model.KthenaChatMessage, error) { + messages := make([]model.KthenaChatMessage, 0) + err := store.db.WithContext(ctx). + Where("session_id = ?", sessionID). + Order("sequence DESC"). + Limit(limit). + Find(&messages).Error + if err != nil { + return nil, err + } + sort.Slice(messages, func(left, right int) bool { + return messages[left].Sequence < messages[right].Sequence + }) + return messages, nil +} + +func (store *kthenaConversationStore) create( + ctx context.Context, + scope *kthenaConversationScope, + clientSessionID, title string, + messages []KthenaConversationMessageReq, +) (model.KthenaChatSession, bool, error) { + if clientSessionID == "" { + clientSessionID = uuid.NewString() + } + now := time.Now().UTC() + if title == "" { + title = kthenaConversationTitle(messages) + } + conversation := model.KthenaChatSession{ + UserID: scope.UserID, + AccountID: scope.AccountID, + Username: scope.Username, + Namespace: scope.Namespace, + ServiceName: scope.ServiceName, + ModelName: scope.ModelName, + BackendType: scope.BackendType, + ClientSessionID: clientSessionID, + Title: title, + MessageCount: len(messages), + CreatedAt: now, + UpdatedAt: now, + } + if len(messages) > 0 { + conversation.LastMessageAt = &now + } + + var existed bool + err := store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + result := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&conversation) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + existed = true + return store.scoped(tx, scope). + Where("client_session_id = ?", clientSessionID). + First(&conversation).Error + } + if len(messages) == 0 { + return nil + } + return tx.Create(kthenaConversationMessages(conversation.ID, messages, now)).Error + }) + return conversation, existed, err +} + +func (store *kthenaConversationStore) update( + ctx context.Context, + scope *kthenaConversationScope, + clientSessionID string, + title *string, + messages *[]KthenaConversationMessageReq, +) (model.KthenaChatSession, error) { + var conversation model.KthenaChatSession + err := store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := store.scoped(tx.Clauses(clause.Locking{Strength: "UPDATE"}), scope). + Where("client_session_id = ?", clientSessionID). + First(&conversation).Error; err != nil { + return err + } + + now := time.Now().UTC() + updates := map[string]any{"updated_at": now} + if title != nil { + conversation.Title = *title + updates["title"] = *title + } + if messages != nil { + if err := tx.Where("session_id = ?", conversation.ID).Delete(&model.KthenaChatMessage{}).Error; err != nil { + return err + } + if len(*messages) > 0 { + if err := tx.Create(kthenaConversationMessages(conversation.ID, *messages, now)).Error; err != nil { + return err + } + } + conversation.MessageCount = len(*messages) + updates["message_count"] = conversation.MessageCount + if len(*messages) == 0 { + conversation.LastMessageAt = nil + updates["last_message_at"] = nil + } else { + conversation.LastMessageAt = &now + updates["last_message_at"] = now + } + if conversation.Title == "" { + conversation.Title = kthenaConversationTitle(*messages) + updates["title"] = conversation.Title + } + } + if err := tx.Model(&conversation).Updates(updates).Error; err != nil { + return err + } + conversation.UpdatedAt = now + return nil + }) + return conversation, err +} + +func (store *kthenaConversationStore) delete( + ctx context.Context, scope *kthenaConversationScope, clientSessionID string, +) error { + return store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var conversation model.KthenaChatSession + if err := store.scoped(tx.Clauses(clause.Locking{Strength: "UPDATE"}), scope). + Where("client_session_id = ?", clientSessionID). + First(&conversation).Error; err != nil { + return err + } + if err := tx.Where("session_id = ?", conversation.ID).Delete(&model.KthenaChatMessage{}).Error; err != nil { + return err + } + return tx.Delete(&conversation).Error + }) +} + +func (store *kthenaConversationStore) findTurn( + ctx context.Context, sessionID uint, clientTurnID string, +) (userMessage, assistantMessage model.KthenaChatMessage, found bool, err error) { + err = store.db.WithContext(ctx). + Where("session_id = ? AND client_turn_id = ?", sessionID, clientTurnID). + First(&userMessage).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return model.KthenaChatMessage{}, model.KthenaChatMessage{}, false, nil + } + if err != nil { + return model.KthenaChatMessage{}, model.KthenaChatMessage{}, false, err + } + err = store.db.WithContext(ctx). + Where("session_id = ? AND sequence = ?", sessionID, userMessage.Sequence+1). + First(&assistantMessage).Error + if err != nil { + return model.KthenaChatMessage{}, model.KthenaChatMessage{}, false, err + } + return userMessage, assistantMessage, true, nil +} + +func (store *kthenaConversationStore) appendTurn( + ctx context.Context, + scope *kthenaConversationScope, + clientSessionID string, + req KthenaConversationTurnReq, + assistant ChatMessage, + rawCompletion []byte, +) (model.KthenaChatSession, model.KthenaChatMessage, bool, error) { + var conversation model.KthenaChatSession + var assistantMessage model.KthenaChatMessage + var alreadySaved bool + err := store.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := store.scoped(tx.Clauses(clause.Locking{Strength: "UPDATE"}), scope). + Where("client_session_id = ?", clientSessionID). + First(&conversation).Error; err != nil { + return err + } + if req.ClientTurnID != "" { + var existingUser model.KthenaChatMessage + err := tx.Where("session_id = ? AND client_turn_id = ?", conversation.ID, req.ClientTurnID). + First(&existingUser).Error + if err == nil { + if err := tx.Where("session_id = ? AND sequence = ?", conversation.ID, existingUser.Sequence+1). + First(&assistantMessage).Error; err != nil { + return err + } + alreadySaved = true + return nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + + var lastSequence int + if err := tx.Model(&model.KthenaChatMessage{}). + Where("session_id = ?", conversation.ID). + Select("COALESCE(MAX(sequence), 0)"). + Scan(&lastSequence).Error; err != nil { + return err + } + now := time.Now().UTC() + var clientTurnID *string + if req.ClientTurnID != "" { + clientTurnID = &req.ClientTurnID + } + userMessage := model.KthenaChatMessage{ + SessionID: conversation.ID, + Sequence: lastSequence + 1, + Role: kthenaConversationRoleUser, + Content: req.Content, + ClientTurnID: clientTurnID, + CreatedAt: now, + } + assistantMessage = model.KthenaChatMessage{ + SessionID: conversation.ID, + Sequence: lastSequence + 2, + Role: nonEmpty(assistant.Role, kthenaConversationRoleAssistant), + Content: assistant.Content, + ResponseJSON: datatypes.JSON(rawCompletion), + CreatedAt: now, + } + if err := tx.Create(&[]model.KthenaChatMessage{userMessage, assistantMessage}).Error; err != nil { + return err + } + conversation.MessageCount = lastSequence + 2 + conversation.LastMessageAt = &now + if conversation.Title == "" { + conversation.Title = kthenaConversationTitle([]KthenaConversationMessageReq{{ + Role: kthenaConversationRoleUser, Content: req.Content, + }}) + } + if err := tx.Model(&conversation).Updates(map[string]any{ + "title": conversation.Title, + "message_count": conversation.MessageCount, + "last_message_at": now, + "updated_at": now, + }).Error; err != nil { + return err + } + conversation.UpdatedAt = now + return nil + }) + return conversation, assistantMessage, alreadySaved, err +} + +func (store *kthenaConversationStore) scoped( + db *gorm.DB, scope *kthenaConversationScope, +) *gorm.DB { + return db.Where( + "user_id = ? AND account_id = ? AND namespace = ? AND service_name = ? AND model_name = ?", + scope.UserID, scope.AccountID, scope.Namespace, scope.ServiceName, scope.ModelName, + ) +} + +func kthenaConversationMessages( + sessionID uint, messages []KthenaConversationMessageReq, createdAt time.Time, +) []model.KthenaChatMessage { + rows := make([]model.KthenaChatMessage, 0, len(messages)) + for index, message := range messages { + rows = append(rows, model.KthenaChatMessage{ + SessionID: sessionID, + Sequence: index + 1, + Role: message.Role, + Content: message.Content, + CreatedAt: createdAt, + }) + } + return rows +} + +func buildKthenaConversationTurnBody( + modelName string, history []model.KthenaChatMessage, req KthenaConversationTurnReq, +) ([]byte, error) { + messages := make([]ChatMessage, 0, len(history)+1) + for _, message := range history { + messages = append(messages, ChatMessage{Role: message.Role, Content: message.Content}) + } + messages = append(messages, ChatMessage{Role: kthenaConversationRoleUser, Content: req.Content}) + return json.Marshal(KthenaProxyReq{ + Model: modelName, + Messages: messages, + Temperature: req.Temperature, + MaxTokens: req.MaxTokens, + }) +} + +func kthenaAssistantMessageFromCompletion(rawCompletion []byte) (ChatMessage, error) { + var completion struct { + Choices []struct { + Message *ChatMessage `json:"message"` + Text string `json:"text"` + } `json:"choices"` + } + if err := json.Unmarshal(rawCompletion, &completion); err != nil { + return ChatMessage{}, err + } + if len(completion.Choices) == 0 { + return ChatMessage{}, bizerr.Internal.K8sServiceError.New("completion has no choices") + } + choice := completion.Choices[0] + if choice.Message != nil { + return ChatMessage{ + Role: nonEmpty(choice.Message.Role, kthenaConversationRoleAssistant), + Content: choice.Message.Content, + }, nil + } + return ChatMessage{Role: kthenaConversationRoleAssistant, Content: choice.Text}, nil +} + +func kthenaConversationFindError(c *gin.Context, err error) { + if errors.Is(err, gorm.ErrRecordNotFound) { + resputil.HandleError(c, bizerr.NotFound.DataBaseNotFound.New("conversation not found")) + return + } + kthenaConversationDatabaseError(c, err, "load conversation failed") +} + +func kthenaConversationFindOrDatabaseError(c *gin.Context, err error, message string) { + if errors.Is(err, gorm.ErrRecordNotFound) { + resputil.HandleError(c, bizerr.NotFound.DataBaseNotFound.New("conversation not found")) + return + } + kthenaConversationDatabaseError(c, err, message) +} + +func kthenaConversationDatabaseError(c *gin.Context, err error, message string) { + klog.Errorf("Kthena conversation database error: %v", err) + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, message)) +} diff --git a/backend/internal/handler/inference_conversation_test.go b/backend/internal/handler/inference_conversation_test.go new file mode 100644 index 000000000..49732da47 --- /dev/null +++ b/backend/internal/handler/inference_conversation_test.go @@ -0,0 +1,293 @@ +package handler + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + controllerfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/service" + "github.com/raids-lab/crater/internal/util" +) + +const ( + kthenaConversationTestNamespace = "crater-workspace" + kthenaConversationTestService = "qwen" + kthenaConversationTestModel = "Qwen/Qwen3-4B" + kthenaConversationTestUsername = "alice" + kthenaConversationTestOtherUser = "bob" + kthenaConversationServedModel = "served-model-name" +) + +//nolint:gocyclo // This API-level test intentionally exercises create, list, update, and access isolation in one user flow. +func TestKthenaConversationHandlersPersistAndScopeByDeploymentUser(t *testing.T) { + router, setToken, db := newKthenaConversationTestRouter(t, nil) + + created := requestKthenaConversation(t, router, http.MethodPost, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations", + `{"sessionId":"client-conversation-a","messages":[{"role":"user","content":"介绍一下 Crater"}]}`, + ) + if created.SessionID != "client-conversation-a" { + t.Fatalf("sessionId = %q", created.SessionID) + } + if created.Namespace != kthenaConversationTestNamespace || created.ServiceName != kthenaConversationTestService || + created.ModelName != kthenaConversationTestModel || created.BackendType != "vLLM" { + t.Fatalf("unexpected conversation scope: %+v", created) + } + if created.MessageCount != 1 || len(created.Messages) != 1 || created.Title != "介绍一下 Crater" { + t.Fatalf("created conversation = %+v", created) + } + + // A same client UUID retry is idempotent inside the current user/deployment/model scope. + retry := requestKthenaConversation(t, router, http.MethodPost, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations", + `{"sessionId":"client-conversation-a","title":"should be ignored"}`, + ) + if retry.Title != created.Title || retry.MessageCount != 1 { + t.Fatalf("idempotent create changed conversation: %+v", retry) + } + + // Seed a foreign-user session in the same deployment scope. The list must not expose it. + foreign := model.KthenaChatSession{ + UserID: 999, + AccountID: 2, + Username: "other-user", + Namespace: kthenaConversationTestNamespace, + ServiceName: kthenaConversationTestService, + ModelName: kthenaConversationTestModel, + BackendType: "vLLM", + ClientSessionID: "foreign-conversation", + } + if err := db.Create(&foreign).Error; err != nil { + t.Fatal(err) + } + + listed := requestKthenaConversationList(t, router, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations?includeMessages=true") + if len(listed) != 1 || listed[0].SessionID != created.SessionID || len(listed[0].Messages) != 1 { + t.Fatalf("list leaked foreign sessions: %+v", listed) + } + + updated := requestKthenaConversation(t, router, http.MethodPatch, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations/client-conversation-a", + `{"title":"部署说明","messages":[{"role":"system","content":"简洁回答"},{"role":"user","content":"模型在哪运行"},{"role":"assistant","content":"运行在 Kthena。"}]}`, + ) + if updated.Title != "部署说明" || updated.MessageCount != 3 || len(updated.Messages) != 3 || + updated.Messages[2].Content != "运行在 Kthena。" { + t.Fatalf("updated conversation = %+v", updated) + } + + // The same deployment is not even discoverable by a different authenticated user. + setToken(util.JWTMessage{UserID: 2, AccountID: 2, Username: kthenaConversationTestOtherUser}) + response := httptest.NewRecorder() + router.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations/client-conversation-a", http.NoBody)) + if response.Code != http.StatusNotFound { + t.Fatalf("foreign user status = %d, body = %s", response.Code, response.Body.String()) + } +} + +//nolint:gocyclo // This API-level test intentionally verifies request forwarding and retry idempotency together. +func TestKthenaConversationTurnPersistsAtomicallyAndRetriesByClientTurnID(t *testing.T) { + var proxyCalls int + router, _, _ := newKthenaConversationTestRouter(t, func( + _ context.Context, method, path string, body []byte, _ http.Header, + ) ([]byte, error) { + proxyCalls++ + if method != http.MethodPost || path != "v1/chat/completions" { + t.Fatalf("proxy target = %s %s", method, path) + } + var request KthenaProxyReq + if err := json.Unmarshal(body, &request); err != nil { + t.Fatal(err) + } + if request.Model != kthenaConversationTestService || len(request.Messages) != 1 || + request.Messages[0].Role != kthenaConversationRoleUser || request.Messages[0].Content != "你好" { + t.Fatalf("unexpected proxy request: %+v", request) + } + return []byte(`{"id":"completion-1","choices":[{"message":{"role":"assistant","content":"你好,我是部署模型。"}}]}`), nil + }) + turn := requestKthenaConversationTurn(t, router, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations/turns", + `{"content":"你好","clientTurnId":"turn-a"}`, + ) + if turn.Conversation.SessionID == "" || turn.Conversation.MessageCount != 2 || + len(turn.Conversation.Messages) != 2 || turn.Assistant.Content != "你好,我是部署模型。" || + string(turn.Completion) == "" { + t.Fatalf("persisted turn = %+v", turn) + } + if turn.Conversation.ModelName != kthenaConversationTestModel { + t.Fatalf("stored served model = %q", turn.Conversation.ModelName) + } + + // The second request is served from storage and never calls the model again. + retry := requestKthenaConversationTurn(t, router, + "/v1/kthena/inference-services/"+kthenaConversationTestService+"/conversations/"+turn.Conversation.SessionID+"/turns", + `{"content":"你好","clientTurnId":"turn-a"}`, + ) + if proxyCalls != 1 || retry.Conversation.MessageCount != 2 || + retry.Assistant.Content != turn.Assistant.Content || string(retry.Completion) == "" { + t.Fatalf("idempotent turn result = %+v, calls = %d", retry, proxyCalls) + } +} + +func TestKthenaConversationRoutesRequireFeatureGate(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_conversation_gate?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}, &model.KthenaChatSession{}, &model.KthenaChatMessage{}); err != nil { + t.Fatal(err) + } + manager := &KthenaMgr{ + configService: service.NewConfigService(query.Use(db)), + conversationStore: newKthenaConversationStore(db), + } + router := gin.New() + manager.RegisterProtected(router.Group("/v1/kthena")) + for _, request := range []struct { + method string + path string + }{ + {http.MethodGet, "/v1/kthena/inference-services/qwen/conversations"}, + {http.MethodGet, "/v1/kthena/inference-services/qwen/conversations/client-a"}, + {http.MethodPost, "/v1/kthena/inference-services/qwen/conversations/turns"}, + } { + response := httptest.NewRecorder() + router.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), request.method, request.path, http.NoBody)) + if response.Code != http.StatusConflict { + t.Fatalf("feature-gated %s %s returned %d: %s", request.method, request.path, response.Code, response.Body.String()) + } + } +} + +func newKthenaConversationTestRouter( + t *testing.T, + proxy func(context.Context, string, string, []byte, http.Header) ([]byte, error), +) (*gin.Engine, func(util.JWTMessage), *gorm.DB) { + t.Helper() + gin.SetMode(gin.TestMode) + db, err := gorm.Open(sqlite.Open("file:kthena_conversation_handlers_"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}, &model.KthenaChatSession{}, &model.KthenaChatMessage{}); err != nil { + t.Fatal(err) + } + configService := service.NewConfigService(query.Use(db)) + if err := configService.SetKthenaInferenceEnabled(t.Context(), true); err != nil { + t.Fatal(err) + } + + booster := newKthenaConversationTestModelBooster() + scheme := runtime.NewScheme() + scheme.AddKnownTypeWithName(modelBoosterGVK, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(modelBoosterListGVK, &unstructured.UnstructuredList{}) + manager := &KthenaMgr{ + client: controllerfake.NewClientBuilder().WithScheme(scheme).WithObjects(booster).Build(), + configService: configService, + conversationStore: newKthenaConversationStore(db), + proxyKthenaRouterFn: proxy, + namespace: "crater-workspace", + } + + currentToken := util.JWTMessage{UserID: 1, AccountID: 2, Username: kthenaConversationTestUsername} + router := gin.New() + router.Use(func(c *gin.Context) { + util.SetJWTContext(c, currentToken) + c.Next() + }) + manager.RegisterProtected(router.Group("/v1/kthena")) + return router, func(token util.JWTMessage) { currentToken = token }, db +} + +func newKthenaConversationTestModelBooster() *unstructured.Unstructured { + booster := &unstructured.Unstructured{Object: map[string]any{ + "spec": map[string]any{ + "backend": map[string]any{ + kthenaSpecTypeKey: "vLLM", + "modelURI": "hf://" + kthenaConversationTestModel, + "workers": []any{map[string]any{ + "config": map[string]any{kthenaConversationServedModel: kthenaConversationTestModel}, + }}, + }, + }, + }} + booster.SetGroupVersionKind(modelBoosterGVK) + booster.SetName(kthenaConversationTestService) + booster.SetNamespace(kthenaConversationTestNamespace) + booster.SetLabels(map[string]string{ + inferenceServiceLabelUserID: "1", + inferenceServiceLabelAccountID: "2", + }) + return booster +} + +func requestKthenaConversation( + t *testing.T, router http.Handler, method, path, body string, +) KthenaConversationResp { + t.Helper() + response := httptest.NewRecorder() + request := httptest.NewRequestWithContext(t.Context(), method, path, bytes.NewBufferString(body)) + request.Header.Set("Content-Type", "application/json") + router.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("%s %s returned %d: %s", method, path, response.Code, response.Body.String()) + } + var payload struct { + Data KthenaConversationResp `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + return payload.Data +} + +func requestKthenaConversationList( + t *testing.T, router http.Handler, path string, +) []KthenaConversationResp { + t.Helper() + response := httptest.NewRecorder() + router.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, path, http.NoBody)) + if response.Code != http.StatusOK { + t.Fatalf("GET %s returned %d: %s", path, response.Code, response.Body.String()) + } + var payload struct { + Data []KthenaConversationResp `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + return payload.Data +} + +func requestKthenaConversationTurn( + t *testing.T, router http.Handler, path, body string, +) KthenaConversationTurnResp { + t.Helper() + response := httptest.NewRecorder() + request := httptest.NewRequestWithContext(t.Context(), http.MethodPost, path, bytes.NewBufferString(body)) + request.Header.Set("Content-Type", "application/json") + router.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("POST %s returned %d: %s", path, response.Code, response.Body.String()) + } + var payload struct { + Data KthenaConversationTurnResp `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + return payload.Data +} diff --git a/backend/internal/handler/inference_service.go b/backend/internal/handler/inference_service.go new file mode 100644 index 000000000..87b3d4bca --- /dev/null +++ b/backend/internal/handler/inference_service.go @@ -0,0 +1,1719 @@ +package handler + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/bizerr" + "github.com/raids-lab/crater/internal/resputil" + "github.com/raids-lab/crater/internal/service" + "github.com/raids-lab/crater/internal/util" + "github.com/raids-lab/crater/pkg/config" +) + +const ( + inferenceServiceLabelManagedBy = "crater.raids.io/managed-by" + inferenceServiceLabelUserID = "crater.raids.io/user-id" + inferenceServiceLabelAccountID = "crater.raids.io/account-id" + inferenceServiceManagedByValue = "inference-service" + + inferenceServiceAnnotationUsername = "crater.raids.io/user" + inferenceServiceAnnotationAccount = "crater.raids.io/account" + inferenceServiceAnnotationSource = "crater.raids.io/model-source" + inferenceServiceAnnotationModelID = "crater.raids.io/platform-model-id" + + kthenaNamespace = "kthena-system" + kthenaRouterService = "kthena-router" + kthenaProxyPrefix = "openai" + kthenaSchedulerName = "volcano" + kthenaSpecTypeKey = "type" + kthenaWorkloadServingAPIGroup = "workload.serving.volcano.sh" + kthenaNetworkingServingAPIGroup = "networking.serving.volcano.sh" + kthenaAPIVersion = "v1alpha1" + kthenaKindModelBooster = "ModelBooster" + kthenaKindModelBoosterList = "ModelBoosterList" + kthenaKindModelServing = "ModelServing" + kthenaKindModelServingList = "ModelServingList" + kthenaKindModelRoute = "ModelRoute" + kthenaKindModelRouteList = "ModelRouteList" + kthenaKindModelServer = "ModelServer" + kthenaKindModelServerList = "ModelServerList" + kthenaBackendVLLM = "vLLM" + kthenaDefaultCacheURI = "hostpath:///tmp/cache" + kthenaDefaultWorkerMemory = "4Gi" + kthenaEnvValueKey = "value" + kthenaResourceCPUKey = "cpu" + kthenaResourceMemoryKey = "memory" + kthenaPhasePending = "Pending" + kthenaPhaseReady = "Ready" + kthenaPhaseActive = "Active" + kthenaPhaseDegraded = "Degraded" + kthenaPhaseProgressing = "Progressing" + kthenaDiagnosticLevelWarning = "warning" + kthenaMaxReplicas int64 = 1_000_000 + kthenaRelatedResourceCapacity = 4 + kthenaLogVerbosity = 4 + kthenaMaxDiagnostics = 10 + kthenaDefaultLogTailLines int64 = 80 + + inferenceModelSourcePlatform = "platform" + inferenceModelSourceExternal = "external" +) + +var ( + modelBoosterGVK = schema.GroupVersionKind{ + Group: kthenaWorkloadServingAPIGroup, + Version: kthenaAPIVersion, + Kind: kthenaKindModelBooster, + } + modelBoosterListGVK = schema.GroupVersionKind{ + Group: kthenaWorkloadServingAPIGroup, + Version: kthenaAPIVersion, + Kind: kthenaKindModelBoosterList, + } + modelServingListGVK = schema.GroupVersionKind{ + Group: kthenaWorkloadServingAPIGroup, + Version: kthenaAPIVersion, + Kind: kthenaKindModelServingList, + } + modelRouteListGVK = schema.GroupVersionKind{ + Group: kthenaNetworkingServingAPIGroup, + Version: kthenaAPIVersion, + Kind: kthenaKindModelRouteList, + } + modelServerListGVK = schema.GroupVersionKind{ + Group: kthenaNetworkingServingAPIGroup, + Version: kthenaAPIVersion, + Kind: kthenaKindModelServerList, + } + inferenceServiceNameRE = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`) +) + +//nolint:gochecknoinits // This is the standard way to register a gin handler. +func init() { + Registers = append(Registers, NewKthenaMgr) +} + +type KthenaMgr struct { + name string + client client.Client + kubeClient kubernetes.Interface + configService *service.ConfigService + conversationStore *kthenaConversationStore + proxyKthenaRouterFn func( + ctx context.Context, + method string, + targetPath string, + body []byte, + headers http.Header, + ) ([]byte, error) + namespace string +} + +func NewKthenaMgr(conf *RegisterConfig) Manager { + return &KthenaMgr{ + name: "kthena", + client: conf.Client, + kubeClient: conf.KubeClient, + configService: conf.ConfigService, + conversationStore: newKthenaConversationStore(query.GetDB()), + namespace: config.GetConfig().Namespaces.Job, + } +} + +func (mgr *KthenaMgr) GetName() string { return mgr.name } + +func (mgr *KthenaMgr) RegisterPublic(_ *gin.RouterGroup) {} + +func (mgr *KthenaMgr) RegisterProtected(g *gin.RouterGroup) { + services := g.Group("inference-services", mgr.requireKthenaInferenceEnabled) + services.POST("", mgr.CreateKthenaService) + services.GET("", mgr.ListKthenaServices) + services.GET(":name/conversations", mgr.ListKthenaConversations) + services.POST(":name/conversations", mgr.CreateKthenaConversation) + // Register the static `turns` route before the parameterized session route + // so an old client may send an empty sessionId in the JSON body. + services.POST(":name/conversations/turns", mgr.CreateKthenaConversationTurnWithoutSession) + services.POST(":name/conversations/:sessionID/turns", mgr.CreateKthenaConversationTurn) + services.GET(":name/conversations/:sessionID", mgr.GetKthenaConversation) + services.PATCH(":name/conversations/:sessionID", mgr.UpdateKthenaConversation) + services.DELETE(":name/conversations/:sessionID", mgr.DeleteKthenaConversation) + services.Any(":name/"+kthenaProxyPrefix+"/*path", mgr.ProxyKthenaService) + services.GET(":name", mgr.GetKthenaService) + services.GET(":name/yaml", mgr.GetKthenaServiceYaml) + services.DELETE(":name", mgr.DeleteKthenaService) +} + +func (mgr *KthenaMgr) RegisterAdmin(g *gin.RouterGroup) { + services := g.Group("inference-services", mgr.requireKthenaInferenceEnabled) + services.GET("", mgr.AdminListKthenaServices) + services.GET(":name", mgr.AdminGetKthenaService) + services.GET(":name/yaml", mgr.AdminGetKthenaServiceYaml) + services.DELETE(":name", mgr.AdminDeleteKthenaService) +} + +func (mgr *KthenaMgr) requireKthenaInferenceEnabled(c *gin.Context) { + if mgr.configService == nil || !mgr.configService.IsKthenaInferenceEnabled(c.Request.Context()) { + resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New("Kthena inference feature is disabled")) + c.Abort() + return + } + c.Next() +} + +type KthenaWorkerReq struct { + Image string `json:"image" binding:"required"` + Replicas int64 `json:"replicas"` + Pods int64 `json:"pods"` + CPU string `json:"cpu"` + Memory string `json:"memory"` + GPU string `json:"gpu"` + GPUModel string `json:"gpuModel"` + Config map[string]string `json:"config,omitempty"` +} + +type CreateKthenaReq struct { + Name string `json:"name" binding:"required"` + ModelSource string `json:"modelSource"` + PlatformModelID uint `json:"platformModelId"` + ModelURI string `json:"modelURI"` + ServedModel string `json:"servedModel"` + BackendType string `json:"backendType"` + CacheURI string `json:"cacheURI"` + Replicas int64 `json:"replicas"` + Env map[string]string `json:"env,omitempty"` + Worker KthenaWorkerReq `json:"worker" binding:"required"` + Selectors []corev1.NodeSelectorRequirement `json:"selectors,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` +} + +type KthenaServiceResp struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Owner string `json:"owner"` + UserInfo model.UserInfo `json:"userInfo"` + ModelSource string `json:"modelSource"` + PlatformModelID uint `json:"platformModelId"` + ModelURI string `json:"modelURI"` + ServedModel string `json:"servedModel"` + BackendType string `json:"backendType"` + CacheURI string `json:"cacheURI"` + Replicas int64 `json:"replicas"` + WorkerImage string `json:"workerImage"` + WorkerReplicas int64 `json:"workerReplicas"` + WorkerCPU string `json:"workerCPU"` + WorkerMemory string `json:"workerMemory"` + WorkerGPU string `json:"workerGPU"` + WorkerGPUModel string `json:"workerGPUModel"` + Env map[string]string `json:"env"` + WorkerConfig map[string]string `json:"workerConfig"` + Phase string `json:"phase"` + Conditions []map[string]any `json:"conditions"` + Resources []KthenaResource `json:"resources"` + RuntimePods []KthenaRuntimePod `json:"runtimePods"` + Diagnostics []KthenaDiagnostic `json:"diagnostics"` + Access KthenaAccess `json:"access"` + Labels map[string]string `json:"labels"` + CreationTimestamp time.Time `json:"createdAt"` +} + +type KthenaResource struct { + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Phase string `json:"phase"` + Ready bool `json:"ready"` + Conditions []map[string]any `json:"conditions"` +} + +type KthenaRuntimePod struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + NodeName string `json:"nodeName"` + PodIP string `json:"podIP,omitempty"` + HostIP string `json:"hostIP,omitempty"` + Phase string `json:"phase"` + Ready bool `json:"ready"` + Restarts int32 `json:"restarts"` + ReadyContainers int `json:"readyContainers"` + TotalContainers int `json:"totalContainers"` +} + +type KthenaAccess struct { + ModelName string `json:"modelName"` + ProxyBaseURL string `json:"proxyBaseURL"` + InternalBaseURL string `json:"internalBaseURL"` + NodePortURL string `json:"nodePortURL,omitempty"` + RouterService string `json:"routerService"` + RouteName string `json:"routeName,omitempty"` + ServerName string `json:"serverName,omitempty"` +} + +type KthenaDiagnostic struct { + Level string `json:"level"` + Reason string `json:"reason"` + Message string `json:"message"` + Details string `json:"details,omitempty"` + Resource string `json:"resource,omitempty"` + Pod string `json:"pod,omitempty"` + Container string `json:"container,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` +} + +type KthenaProxyReq struct { + Model string `json:"model,omitempty"` + Messages []ChatMessage `json:"messages,omitempty"` + Prompt any `json:"prompt,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + MaxTokens *int64 `json:"max_tokens,omitempty"` + Stream bool `json:"stream,omitempty"` + Extra map[string]any `json:"-"` +} + +type ChatMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// CreateKthenaService godoc +// +// @Summary Create inference service +// @Description Create a Kthena ModelBooster-backed inference service. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body CreateKthenaReq true "Create inference service request" +// @Success 200 {object} resputil.Response[KthenaServiceResp] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services [post] +func (mgr *KthenaMgr) CreateKthenaService(c *gin.Context) { + var req CreateKthenaReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid request")) + return + } + token := util.GetToken(c) + if err := validateCreateKthenaReq(c.Request.Context(), &req, token); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + + obj := buildModelBoosterObject(&req, token, mgr.namespace) + if err := mgr.client.Create(c.Request.Context(), obj); err != nil { + if errors.IsAlreadyExists(err) { + resputil.HandleError(c, bizerr.Conflict.ResourceAlreadyExists.Wrap( + err, + fmt.Sprintf("inference service %q already exists", req.Name), + )) + return + } + klog.Errorf("create inference service failed: %v", err) + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "create inference service failed")) + return + } + + resp, err := mgr.modelBoosterToResp(c.Request.Context(), obj) + if err != nil { + resputil.HandleError(c, bizerr.Internal.ServiceError.Wrap( + err, + "created inference service but failed to parse response", + )) + return + } + resputil.Success(c, resp) +} + +// ListKthenaServices godoc +// +// @Summary List my inference services +// @Description List Kthena inference services owned by the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {object} resputil.Response[[]KthenaServiceResp] +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services [get] +func (mgr *KthenaMgr) ListKthenaServices(c *gin.Context) { + token := util.GetToken(c) + services, err := mgr.listKthenaServices( + c.Request.Context(), + client.MatchingLabels{ + inferenceServiceLabelManagedBy: inferenceServiceManagedByValue, + inferenceServiceLabelUserID: strconv.FormatUint(uint64(token.UserID), 10), + inferenceServiceLabelAccountID: strconv.FormatUint(uint64(token.AccountID), 10), + }, + ) + if err != nil { + klog.Errorf("list inference services failed: %v", err) + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "list inference services failed")) + return + } + resputil.Success(c, services) +} + +// AdminListKthenaServices godoc +// +// @Summary List all inference services +// @Description List all Kthena inference services managed by Crater. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {object} resputil.Response[[]KthenaServiceResp] +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/admin/kthena/inference-services [get] +func (mgr *KthenaMgr) AdminListKthenaServices(c *gin.Context) { + services, err := mgr.listKthenaServices( + c.Request.Context(), + client.MatchingLabels{inferenceServiceLabelManagedBy: inferenceServiceManagedByValue}, + ) + if err != nil { + klog.Errorf("admin list inference services failed: %v", err) + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "list inference services failed")) + return + } + resputil.Success(c, services) +} + +// GetKthenaService godoc +// +// @Summary Get inference service +// @Description Get a Kthena inference service owned by the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[KthenaServiceResp] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services/{name} [get] +func (mgr *KthenaMgr) GetKthenaService(c *gin.Context) { + mgr.getKthenaService(c, false, false) +} + +// AdminGetKthenaService godoc +// +// @Summary Get inference service as admin +// @Description Get any Kthena inference service managed by Crater. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[KthenaServiceResp] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/admin/kthena/inference-services/{name} [get] +func (mgr *KthenaMgr) AdminGetKthenaService(c *gin.Context) { + mgr.getKthenaService(c, true, false) +} + +// GetKthenaServiceYaml godoc +// +// @Summary Get inference service YAML +// @Description Get raw Kthena ModelBooster object for an inference service owned by the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[any] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services/{name}/yaml [get] +func (mgr *KthenaMgr) GetKthenaServiceYaml(c *gin.Context) { + mgr.getKthenaService(c, false, true) +} + +// AdminGetKthenaServiceYaml godoc +// +// @Summary Get inference service YAML as admin +// @Description Get raw Kthena ModelBooster object for any inference service managed by Crater. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[any] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/admin/kthena/inference-services/{name}/yaml [get] +func (mgr *KthenaMgr) AdminGetKthenaServiceYaml(c *gin.Context) { + mgr.getKthenaService(c, true, true) +} + +// DeleteKthenaService godoc +// +// @Summary Delete inference service +// @Description Delete a Kthena inference service owned by the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[string] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services/{name} [delete] +func (mgr *KthenaMgr) DeleteKthenaService(c *gin.Context) { + mgr.deleteKthenaService(c, false) +} + +// AdminDeleteKthenaService godoc +// +// @Summary Delete inference service as admin +// @Description Delete any Kthena inference service managed by Crater. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Success 200 {object} resputil.Response[string] +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/admin/kthena/inference-services/{name} [delete] +func (mgr *KthenaMgr) AdminDeleteKthenaService(c *gin.Context) { + mgr.deleteKthenaService(c, true) +} + +func (mgr *KthenaMgr) listKthenaServices( + ctx context.Context, + labels client.MatchingLabels, +) ([]KthenaServiceResp, error) { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(modelBoosterListGVK) + if err := mgr.client.List(ctx, list, client.InNamespace(mgr.namespace), labels); err != nil { + return nil, err + } + + services := make([]KthenaServiceResp, 0, len(list.Items)) + for i := range list.Items { + resp, err := mgr.modelBoosterToResp(ctx, &list.Items[i]) + if err != nil { + klog.Warningf( + "skip malformed ModelBooster %s/%s: %v", + list.Items[i].GetNamespace(), + list.Items[i].GetName(), + err, + ) + continue + } + services = append(services, *resp) + } + return services, nil +} + +func (mgr *KthenaMgr) getKthenaService(c *gin.Context, admin, raw bool) { + obj, ok := mgr.loadKthenaService(c, admin) + if !ok { + return + } + if raw { + resputil.Success(c, obj.Object) + return + } + resp, err := mgr.modelBoosterToResp(c.Request.Context(), obj) + if err != nil { + resputil.HandleError(c, bizerr.Internal.ServiceError.Wrap(err, "failed to parse inference service")) + return + } + resputil.Success(c, resp) +} + +func (mgr *KthenaMgr) deleteKthenaService(c *gin.Context, admin bool) { + obj, ok := mgr.loadKthenaService(c, admin) + if !ok { + return + } + if err := mgr.client.Delete(c.Request.Context(), obj); err != nil { + klog.Errorf("delete inference service failed: %v", err) + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "delete inference service failed")) + return + } + resputil.Success(c, "inference service deleted") +} + +// ProxyKthenaService godoc +// +// @Summary Proxy OpenAI-compatible inference request +// @Description Proxy an OpenAI-compatible request to kthena-router for an inference service owned by the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param name path string true "Inference service name" +// @Param path path string true "OpenAI-compatible API path" +// @Param request body KthenaProxyReq false "OpenAI-compatible request body" +// @Success 200 {object} any +// @Failure 400 {object} resputil.Response[any] "Request parameter error" +// @Failure 404 {object} any "Model route or runtime pod not found" +// @Failure 500 {object} resputil.Response[any] "Other errors" +// @Router /v1/kthena/inference-services/{name}/openai/{path} [post] +func (mgr *KthenaMgr) ProxyKthenaService(c *gin.Context) { + obj, ok := mgr.loadKthenaService(c, false) + if !ok { + return + } + + targetPath := strings.TrimPrefix(c.Param("path"), "/") + if targetPath == "" { + targetPath = "v1/chat/completions" + } + if !strings.HasPrefix(targetPath, "v1/") { + targetPath = "v1/" + targetPath + } + + body, err := io.ReadAll(c.Request.Body) + if err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "failed to read request body")) + return + } + body, err = withDefaultModel(body, servedModelFromModelBooster(obj)) + if err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + + rawResp, err := mgr.proxyKthenaRouter(c.Request.Context(), c.Request.Method, targetPath, body, c.Request.Header) + if err != nil { + klog.Errorf("proxy inference request failed: %v", err) + // DoRaw preserves the router response body even for non-2xx responses. + // Return that status and body to the caller so errors such as missing + // runtime pods remain actionable instead of becoming a generic 500. + if len(rawResp) > 0 { + c.Data(kthenaProxyHTTPStatus(err), "application/json", rawResp) + return + } + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "proxy inference request failed")) + return + } + c.Data(http.StatusOK, "application/json", rawResp) +} + +func (mgr *KthenaMgr) proxyKthenaRouter( + ctx context.Context, + method string, + targetPath string, + body []byte, + headers http.Header, +) ([]byte, error) { + if mgr.proxyKthenaRouterFn != nil { + return mgr.proxyKthenaRouterFn(ctx, method, targetPath, body, headers) + } + if mgr.kubeClient == nil { + return nil, bizerr.Internal.K8sServiceError.New("kubernetes client is not initialized") + } + req := mgr.kubeClient.CoreV1().RESTClient(). + Verb(method). + Namespace(kthenaNamespace). + Resource("services"). + Name(kthenaRouterService + ":http"). + SubResource("proxy"). + Suffix(strings.Split(targetPath, "/")...) + for key, values := range headers { + canonicalKey := http.CanonicalHeaderKey(key) + switch canonicalKey { + case "Authorization", "Cookie", "Host", "Content-Length": + continue + } + for _, value := range values { + req.SetHeader(canonicalKey, value) + } + } + req.SetHeader("Content-Type", "application/json") + if len(body) > 0 { + req.Body(body) + } + return req.DoRaw(ctx) +} + +func kthenaProxyHTTPStatus(err error) int { + type apiStatus interface { + Status() metav1.Status + } + if statusErr, ok := err.(apiStatus); ok { + statusCode := int(statusErr.Status().Code) + if statusCode >= http.StatusBadRequest && statusCode <= 599 { + return statusCode + } + } + return http.StatusBadGateway +} + +func (mgr *KthenaMgr) loadKthenaService(c *gin.Context, admin bool) (*unstructured.Unstructured, bool) { + var req struct { + Name string `uri:"name" binding:"required"` + } + if err := c.ShouldBindUri(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid service name")) + return nil, false + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(modelBoosterGVK) + if err := mgr.client.Get(c.Request.Context(), client.ObjectKey{Namespace: mgr.namespace, Name: req.Name}, obj); err != nil { + if errors.IsNotFound(err) { + resputil.HandleError(c, bizerr.NotFound.K8sResourceNotFound.Wrap(err, "inference service not found")) + return nil, false + } + klog.Errorf("get inference service failed: %v", err) + resputil.HandleError(c, bizerr.Internal.K8sServiceError.Wrap(err, "get inference service failed")) + return nil, false + } + if !admin && !canAccessKthenaService(c, obj) { + resputil.HandleError(c, bizerr.NotFound.K8sResourceNotFound.New("inference service not found")) + return nil, false + } + + return obj, true +} + +func canAccessKthenaService(c *gin.Context, obj *unstructured.Unstructured) bool { + token := util.GetToken(c) + labels := obj.GetLabels() + return labels[inferenceServiceLabelUserID] == strconv.FormatUint(uint64(token.UserID), 10) && + labels[inferenceServiceLabelAccountID] == strconv.FormatUint(uint64(token.AccountID), 10) +} + +//nolint:gocyclo // Each field is normalized and validated in a fixed order so API clients receive precise errors. +func validateCreateKthenaReq(ctx context.Context, req *CreateKthenaReq, token util.JWTMessage) error { + req.Name = strings.TrimSpace(req.Name) + req.ModelSource = strings.TrimSpace(req.ModelSource) + req.ModelURI = strings.TrimSpace(req.ModelURI) + req.ServedModel = strings.TrimSpace(req.ServedModel) + req.BackendType = strings.TrimSpace(req.BackendType) + req.CacheURI = strings.TrimSpace(req.CacheURI) + req.Worker.Image = strings.TrimSpace(req.Worker.Image) + + if !inferenceServiceNameRE.MatchString(req.Name) || len(req.Name) > 63 { + return bizerr.BadRequest.ParameterError.New( + "name must be a valid Kubernetes name with lowercase letters, digits, or hyphens", + ) + } + if req.ModelSource == "" { + req.ModelSource = inferenceModelSourcePlatform + } + if req.ModelSource == inferenceModelSourcePlatform { + if req.PlatformModelID == 0 { + return bizerr.BadRequest.MissingParameter.New("platform model is required") + } + dataset, err := loadAccessibleModelDataset(ctx, req.PlatformModelID, token) + if err != nil { + return err + } + req.ModelURI = datasetToKthenaModelURI(dataset) + if req.ServedModel == "" { + req.ServedModel = dataset.Name + } + req.CacheURI = datasetModelCacheURI() + } else if req.ModelSource != inferenceModelSourceExternal { + return bizerr.BadRequest.ParameterError.New("modelSource must be platform or external") + } + if req.ModelURI == "" { + return bizerr.BadRequest.MissingParameter.New("modelURI is required") + } + if !strings.HasPrefix(req.ModelURI, "hf://") && + !strings.HasPrefix(req.ModelURI, "s3://") && + !strings.HasPrefix(req.ModelURI, "pvc://") && + !strings.HasPrefix(req.ModelURI, "ms://") { + return bizerr.BadRequest.ParameterError.New("modelURI must start with hf://, s3://, pvc://, or ms://") + } + if req.BackendType == "" { + req.BackendType = kthenaBackendVLLM + } + if req.CacheURI == "" { + req.CacheURI = kthenaDefaultCacheURI + } + if !strings.HasPrefix(req.CacheURI, "hostpath://") && !strings.HasPrefix(req.CacheURI, "pvc://") { + return bizerr.BadRequest.ParameterError.New("cacheURI must start with hostpath:// or pvc://") + } + if req.BackendType != kthenaBackendVLLM { + // Kthena v1.0.0's published CRD accepts vLLM and + // vLLMDisaggregated. CreateKthenaReq represents a single server worker, + // so only the non-disaggregated vLLM shape can be generated here. + return bizerr.BadRequest.ParameterError.New("backendType must be vLLM for Kthena v1.0.0") + } + if req.Replicas <= 0 { + req.Replicas = 1 + } + if req.Replicas > kthenaMaxReplicas { + return bizerr.BadRequest.ParameterError.New("replicas cannot exceed 1000000") + } + if req.Worker.Image == "" { + return bizerr.BadRequest.MissingParameter.New("worker.image is required") + } + if req.Worker.Replicas <= 0 { + req.Worker.Replicas = 1 + } + if req.Worker.Replicas > kthenaMaxReplicas { + return bizerr.BadRequest.ParameterError.New("worker.replicas cannot exceed 1000000") + } + if req.Worker.Pods <= 0 { + req.Worker.Pods = 1 + } + if req.Worker.Pods > kthenaMaxReplicas { + return bizerr.BadRequest.ParameterError.New("worker.pods cannot exceed 1000000") + } + if req.Worker.CPU == "" { + req.Worker.CPU = "2" + } + if req.Worker.Memory == "" { + req.Worker.Memory = kthenaDefaultWorkerMemory + } + if req.Worker.Config == nil { + req.Worker.Config = map[string]string{} + } + if req.ServedModel == "" { + req.ServedModel = inferServedModelName(req.ModelURI) + } + if _, ok := req.Worker.Config["served-model-name"]; !ok && req.ServedModel != "" { + req.Worker.Config["served-model-name"] = req.ServedModel + } + return nil +} + +func buildModelBoosterObject(req *CreateKthenaReq, token util.JWTMessage, namespace string) *unstructured.Unstructured { + labels := map[string]string{ + inferenceServiceLabelManagedBy: inferenceServiceManagedByValue, + inferenceServiceLabelUserID: strconv.FormatUint(uint64(token.UserID), 10), + inferenceServiceLabelAccountID: strconv.FormatUint(uint64(token.AccountID), 10), + } + annotations := map[string]string{ + inferenceServiceAnnotationUsername: token.Username, + inferenceServiceAnnotationAccount: token.AccountName, + inferenceServiceAnnotationSource: req.ModelSource, + } + if req.PlatformModelID > 0 { + annotations[inferenceServiceAnnotationModelID] = strconv.FormatUint(uint64(req.PlatformModelID), 10) + } + + env := make([]any, 0, len(req.Env)) + for name, value := range req.Env { + env = append(env, map[string]any{"name": name, kthenaEnvValueKey: value}) + } + + resources := map[string]any{ + "requests": map[string]any{ + kthenaResourceCPUKey: req.Worker.CPU, + kthenaResourceMemoryKey: req.Worker.Memory, + }, + "limits": map[string]any{ + kthenaResourceCPUKey: req.Worker.CPU, + kthenaResourceMemoryKey: req.Worker.Memory, + }, + } + if strings.TrimSpace(req.Worker.GPU) != "" && req.Worker.GPU != "0" { + gpuModel := strings.TrimSpace(req.Worker.GPUModel) + if gpuModel == "" { + gpuModel = "nvidia.com/gpu" + } + resources["limits"].(map[string]any)[gpuModel] = req.Worker.GPU + } + + workerConfig := make(map[string]any, len(req.Worker.Config)) + for key, value := range req.Worker.Config { + workerConfig[key] = value + } + + worker := map[string]any{ + kthenaSpecTypeKey: "server", + "image": req.Worker.Image, + "replicas": req.Worker.Replicas, + "pods": req.Worker.Pods, + "config": workerConfig, + "resources": resources, + } + if affinity := buildWorkerAffinity(req.Selectors); affinity != nil { + worker["affinity"] = affinity + } + if len(req.Tolerations) > 0 { + worker["tolerations"] = req.Tolerations + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(modelBoosterGVK) + obj.SetName(req.Name) + obj.SetNamespace(namespace) + obj.SetLabels(labels) + obj.SetAnnotations(annotations) + obj.Object["spec"] = map[string]any{ + "backend": map[string]any{ + "name": "backend1", + kthenaSpecTypeKey: req.BackendType, + "modelURI": req.ModelURI, + "cacheURI": req.CacheURI, + "replicas": req.Replicas, + "schedulerName": kthenaSchedulerName, + "env": env, + "workers": []any{worker}, + }, + } + return obj +} + +func (mgr *KthenaMgr) modelBoosterToResp( + ctx context.Context, + obj *unstructured.Unstructured, +) (*KthenaServiceResp, error) { + backend, ok, err := unstructured.NestedMap(obj.Object, "spec", "backend") + if err != nil || !ok { + return nil, bizerr.Internal.K8sServiceError.New("spec.backend is missing") + } + + workers, _, _ := unstructured.NestedSlice(obj.Object, "spec", "backend", "workers") + var worker map[string]any + if len(workers) > 0 { + worker, _ = workers[0].(map[string]any) + } + if worker == nil { + worker = map[string]any{} + } + + configMap, _, _ := unstructured.NestedStringMap(worker, "config") + envMap := envSliceToMap(backend["env"]) + conditions, _ := normalizeConditions(obj) + phase, _, _ := unstructured.NestedString(obj.Object, "status", "phase") + if phase == "" { + phase = conditionPhase(conditions) + } + servedModel := configMap["served-model-name"] + if servedModel == "" { + servedModel = inferServedModelName(stringValue(backend["modelURI"])) + } + + resources := mgr.relatedKthenaResources(ctx, obj, servedModel) + runtimePods := mgr.relatedKthenaRuntimePods(ctx, resources) + access := mgr.buildKthenaAccess(ctx, obj, resources) + diagnostics := mgr.relatedKthenaDiagnostics(ctx, resources) + if phase == kthenaPhasePending { + phase = aggregateInferencePhase(conditions, resources) + } + phase = runtimeAwareInferencePhase(phase, resources, runtimePods) + if phase == kthenaPhaseDegraded && !hasReadyRuntimePod(runtimePods) { + diagnostics = append(diagnostics, KthenaDiagnostic{ + Level: kthenaDiagnosticLevelWarning, + Reason: "RuntimePodsUnavailable", + Message: kthenaKindModelServing + " reports ready, but no ready runtime pod was found", + Resource: kthenaKindModelBooster + "/" + obj.GetNamespace() + "/" + obj.GetName(), + }) + } + owner, userInfo := kthenaServiceOwner(obj) + + return &KthenaServiceResp{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + Owner: owner, + UserInfo: userInfo, + ModelSource: modelSourceFromObject(obj), + PlatformModelID: platformModelIDFromObject(obj), + ModelURI: stringValue(backend["modelURI"]), + ServedModel: servedModel, + BackendType: stringValue(backend[kthenaSpecTypeKey]), + CacheURI: stringValue(backend["cacheURI"]), + Replicas: int64Value(backend["replicas"]), + WorkerImage: stringValue(worker["image"]), + WorkerReplicas: int64Value(worker["replicas"]), + WorkerCPU: nestedResourceValue(worker, kthenaResourceCPUKey), + WorkerMemory: nestedResourceValue(worker, kthenaResourceMemoryKey), + WorkerGPU: firstGPUResourceValue(worker), + WorkerGPUModel: firstGPUResourceName(worker), + Env: envMap, + WorkerConfig: configMap, + Phase: phase, + Conditions: conditions, + Resources: resources, + RuntimePods: runtimePods, + Diagnostics: diagnostics, + Access: access, + Labels: obj.GetLabels(), + CreationTimestamp: obj.GetCreationTimestamp().Time, + }, nil +} + +// kthenaServiceOwner reads the creator identity persisted on the ModelBooster +// at creation time. Keeping this data on the Kubernetes object lets a list +// response expose UserInfo without an additional database lookup for each +// deployment. +func kthenaServiceOwner(obj *unstructured.Unstructured) (string, model.UserInfo) { + if obj == nil { + return "", model.UserInfo{} + } + username := strings.TrimSpace(obj.GetAnnotations()[inferenceServiceAnnotationUsername]) + return username, model.UserInfo{Username: username} +} + +func (mgr *KthenaMgr) relatedKthenaResources( + ctx context.Context, + booster *unstructured.Unstructured, + servedModel string, +) []KthenaResource { + resources := make([]KthenaResource, 0, kthenaRelatedResourceCapacity) + resources = append(resources, kthenaResourceFromObject(kthenaKindModelBooster, booster)) + resources = append(resources, mgr.listRelatedResources(ctx, modelServingListGVK, kthenaKindModelServing, booster, servedModel)...) + resources = append(resources, mgr.listRelatedResources(ctx, modelServerListGVK, kthenaKindModelServer, booster, servedModel)...) + resources = append(resources, mgr.listRelatedResources(ctx, modelRouteListGVK, kthenaKindModelRoute, booster, servedModel)...) + return resources +} + +func (mgr *KthenaMgr) listRelatedResources( + ctx context.Context, + gvk schema.GroupVersionKind, + kind string, + booster *unstructured.Unstructured, + servedModel string, +) []KthenaResource { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + if err := mgr.client.List(ctx, list, client.InNamespace(booster.GetNamespace())); err != nil { + klog.V(kthenaLogVerbosity).Infof( + "list %s for inference service %s/%s failed: %v", + kind, + booster.GetNamespace(), + booster.GetName(), + err, + ) + return nil + } + resources := make([]KthenaResource, 0, len(list.Items)) + for i := range list.Items { + if isRelatedKthenaObject(&list.Items[i], booster, servedModel) { + resources = append(resources, kthenaResourceFromObject(kind, &list.Items[i])) + } + } + return resources +} + +func (mgr *KthenaMgr) relatedKthenaDiagnostics( + ctx context.Context, + resources []KthenaResource, +) []KthenaDiagnostic { + if mgr.kubeClient == nil { + return nil + } + seen := map[string]struct{}{} + diagnostics := make([]KthenaDiagnostic, 0) + for _, resource := range resources { + if resource.Kind != kthenaKindModelServing { + continue + } + pods, err := mgr.kubeClient.CoreV1().Pods(resource.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("modelserving.volcano.sh/name=%s", resource.Name), + }) + if err != nil { + klog.V(kthenaLogVerbosity).Infof( + "list pods for kthena %s %s/%s failed: %v", + kthenaKindModelServing, + resource.Namespace, + resource.Name, + err, + ) + continue + } + for i := range pods.Items { + pod := &pods.Items[i] + key := string(pod.UID) + if key == "" { + key = pod.Namespace + "/" + pod.Name + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + diagnostics = append(diagnostics, mgr.diagnosticsFromPod(ctx, pod)...) + diagnostics = append(diagnostics, mgr.diagnosticsFromPodEvents(ctx, pod)...) + } + } + if len(diagnostics) == 0 { + return nil + } + sort.SliceStable(diagnostics, func(i, j int) bool { + return diagnostics[i].Timestamp.After(diagnostics[j].Timestamp) + }) + if len(diagnostics) > kthenaMaxDiagnostics { + return diagnostics[:kthenaMaxDiagnostics] + } + return diagnostics +} + +func (mgr *KthenaMgr) relatedKthenaRuntimePods( + ctx context.Context, + resources []KthenaResource, +) []KthenaRuntimePod { + if mgr.kubeClient == nil { + return nil + } + seen := map[string]struct{}{} + runtimePods := make([]KthenaRuntimePod, 0) + for _, resource := range resources { + if resource.Kind != kthenaKindModelServing { + continue + } + pods, err := mgr.kubeClient.CoreV1().Pods(resource.Namespace).List(ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("modelserving.volcano.sh/name=%s", resource.Name), + }) + if err != nil { + klog.V(kthenaLogVerbosity).Infof( + "list pods for kthena %s %s/%s failed: %v", + kthenaKindModelServing, + resource.Namespace, + resource.Name, + err, + ) + continue + } + for i := range pods.Items { + pod := &pods.Items[i] + key := string(pod.UID) + if key == "" { + key = pod.Namespace + "/" + pod.Name + } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} + runtimePods = append(runtimePods, kthenaRuntimePodFromPod(pod)) + } + } + if len(runtimePods) == 0 { + return nil + } + sort.SliceStable(runtimePods, func(i, j int) bool { + return runtimePods[i].Name < runtimePods[j].Name + }) + return runtimePods +} + +func kthenaRuntimePodFromPod(pod *corev1.Pod) KthenaRuntimePod { + readyContainers := 0 + totalContainers := len(pod.Status.ContainerStatuses) + restarts := int32(0) + for i := range pod.Status.ContainerStatuses { + status := &pod.Status.ContainerStatuses[i] + if status.Ready { + readyContainers++ + } + restarts += status.RestartCount + } + return KthenaRuntimePod{ + Name: pod.Name, + Namespace: pod.Namespace, + NodeName: pod.Spec.NodeName, + PodIP: pod.Status.PodIP, + HostIP: pod.Status.HostIP, + Phase: string(pod.Status.Phase), + Ready: isPodReady(pod), + Restarts: restarts, + ReadyContainers: readyContainers, + TotalContainers: totalContainers, + } +} + +func isPodReady(pod *corev1.Pod) bool { + for _, condition := range pod.Status.Conditions { + if condition.Type == corev1.PodReady { + return condition.Status == corev1.ConditionTrue + } + } + return false +} + +func (mgr *KthenaMgr) diagnosticsFromPod(ctx context.Context, pod *corev1.Pod) []KthenaDiagnostic { + if pod == nil { + return nil + } + diagnostics := make([]KthenaDiagnostic, 0) + resource := "Pod/" + pod.Namespace + "/" + pod.Name + if pod.Status.Phase == corev1.PodPending { + for _, condition := range pod.Status.Conditions { + if condition.Type != corev1.PodScheduled || condition.Status != corev1.ConditionFalse { + continue + } + diagnostics = append(diagnostics, KthenaDiagnostic{ + Level: kthenaDiagnosticLevelWarning, + Reason: nonEmpty(condition.Reason, "PodSchedulingFailed"), + Message: condition.Message, + Resource: resource, + Pod: pod.Name, + Timestamp: condition.LastTransitionTime.Time, + }) + } + } + for i := range pod.Status.InitContainerStatuses { + status := &pod.Status.InitContainerStatuses[i] + logTail := mgr.containerLogTail(ctx, pod, status.Name, status.RestartCount) + diagnostics = append(diagnostics, diagnosticsFromContainerStatus(pod, status, true, logTail)...) + } + for i := range pod.Status.ContainerStatuses { + status := &pod.Status.ContainerStatuses[i] + logTail := mgr.containerLogTail(ctx, pod, status.Name, status.RestartCount) + diagnostics = append(diagnostics, diagnosticsFromContainerStatus(pod, status, false, logTail)...) + } + return diagnostics +} + +func diagnosticsFromContainerStatus( + pod *corev1.Pod, + status *corev1.ContainerStatus, + initContainer bool, + logTail string, +) []KthenaDiagnostic { + if pod == nil || status == nil { + return nil + } + resource := "Pod/" + pod.Namespace + "/" + pod.Name + containerKind := "container" + reasonPrefix := "Runtime" + if initContainer { + containerKind = "initContainer" + reasonPrefix = "InitContainer" + } + if waiting := status.State.Waiting; waiting != nil { + return []KthenaDiagnostic{{ + Level: kthenaDiagnosticLevelWarning, + Reason: nonEmpty(waiting.Reason, reasonPrefix+"Waiting"), + Message: nonEmpty(waiting.Message, fmt.Sprintf("%s %q is waiting", containerKind, status.Name)), + Details: logTail, + Resource: resource, + Pod: pod.Name, + Container: status.Name, + }} + } + if terminated := status.State.Terminated; terminated != nil && terminated.ExitCode != 0 { + return []KthenaDiagnostic{{ + Level: "error", + Reason: nonEmpty( + terminated.Reason, + reasonPrefix+"Failed", + ), + Message: nonEmpty( + terminated.Message, + fmt.Sprintf("%s %q terminated with exit code %d", containerKind, status.Name, terminated.ExitCode), + ), + Details: logTail, + Resource: resource, + Pod: pod.Name, + Container: status.Name, + Timestamp: terminated.FinishedAt.Time, + }} + } + return nil +} + +func (mgr *KthenaMgr) containerLogTail( + ctx context.Context, + pod *corev1.Pod, + container string, + restartCount int32, +) string { + if mgr.kubeClient == nil || pod == nil || container == "" { + return "" + } + if logs := mgr.readContainerLogTail(ctx, pod.Namespace, pod.Name, container, false); logs != "" { + return logs + } + if restartCount > 0 { + return mgr.readContainerLogTail(ctx, pod.Namespace, pod.Name, container, true) + } + return "" +} + +func (mgr *KthenaMgr) readContainerLogTail( + ctx context.Context, + namespace string, + pod string, + container string, + previous bool, +) string { + tailLines := kthenaDefaultLogTailLines + req := mgr.kubeClient.CoreV1().Pods(namespace).GetLogs(pod, &corev1.PodLogOptions{ + Container: container, + Previous: previous, + TailLines: &tailLines, + }) + stream, err := req.Stream(ctx) + if err != nil { + klog.V(kthenaLogVerbosity).Infof( + "read logs for kthena pod %s/%s container %s previous=%t failed: %v", + namespace, + pod, + container, + previous, + err, + ) + return "" + } + defer func() { + if closeErr := stream.Close(); closeErr != nil { + klog.V(kthenaLogVerbosity).Infof( + "close log stream for kthena pod %s/%s container %s failed: %v", + namespace, + pod, + container, + closeErr, + ) + } + }() + data, err := io.ReadAll(stream) + if err != nil { + klog.V(kthenaLogVerbosity).Infof( + "read log stream for kthena pod %s/%s container %s failed: %v", + namespace, + pod, + container, + err, + ) + return "" + } + return strings.TrimSpace(string(data)) +} + +func (mgr *KthenaMgr) diagnosticsFromPodEvents(ctx context.Context, pod *corev1.Pod) []KthenaDiagnostic { + events, err := mgr.kubeClient.CoreV1().Events(pod.Namespace).List(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("involvedObject.name=%s", pod.Name), + }) + if err != nil { + klog.V(kthenaLogVerbosity).Infof("list events for kthena pod %s/%s failed: %v", pod.Namespace, pod.Name, err) + return nil + } + diagnostics := make([]KthenaDiagnostic, 0, len(events.Items)) + for i := range events.Items { + event := &events.Items[i] + // Pod names are reused when a ModelBooster is recreated. Events from a + // previous pod with the same name can remain in the namespace and must + // not make the replacement deployment appear unhealthy. + if event.InvolvedObject.UID != pod.UID { + continue + } + if event.Type != corev1.EventTypeWarning { + continue + } + diagnostics = append(diagnostics, KthenaDiagnostic{ + Level: kthenaDiagnosticLevelWarning, + Reason: nonEmpty(event.Reason, "PodWarning"), + Message: event.Message, + Resource: "Pod/" + pod.Namespace + "/" + pod.Name, + Pod: pod.Name, + Timestamp: event.LastTimestamp.Time, + }) + } + return diagnostics +} + +func isRelatedKthenaObject(obj, booster *unstructured.Unstructured, servedModel string) bool { + name := obj.GetName() + boosterName := booster.GetName() + if name == boosterName || strings.HasPrefix(name, boosterName+"-") { + return true + } + labels := obj.GetLabels() + if labels["workload.serving.volcano.sh/model-name"] == boosterName { + return true + } + for _, owner := range obj.GetOwnerReferences() { + if owner.Kind == kthenaKindModelBooster && owner.Name == boosterName { + return true + } + } + if labels[inferenceServiceLabelManagedBy] == inferenceServiceManagedByValue && + labels[inferenceServiceLabelUserID] == booster.GetLabels()[inferenceServiceLabelUserID] && + labels[inferenceServiceLabelAccountID] == booster.GetLabels()[inferenceServiceLabelAccountID] { + return true + } + if servedModel != "" && (name == servedModel || strings.Contains(name, sanitizeKubeName(servedModel))) { + return true + } + return false +} + +func kthenaResourceFromObject(kind string, obj *unstructured.Unstructured) KthenaResource { + conditions, _ := normalizeConditions(obj) + phase, _, _ := unstructured.NestedString(obj.Object, "status", "phase") + if phase == "" { + phase = conditionPhase(conditions) + } + return KthenaResource{ + Kind: kind, + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + Phase: phase, + Ready: phase == kthenaPhaseReady || phase == kthenaPhaseActive, + Conditions: conditions, + } +} + +func (mgr *KthenaMgr) buildKthenaAccess( + ctx context.Context, + booster *unstructured.Unstructured, + resources []KthenaResource, +) KthenaAccess { + access := KthenaAccess{ + ModelName: booster.GetName(), + ProxyBaseURL: fmt.Sprintf("/v1/kthena/inference-services/%s/%s/v1", booster.GetName(), kthenaProxyPrefix), + InternalBaseURL: fmt.Sprintf("http://%s.%s.svc.cluster.local/v1", kthenaRouterService, kthenaNamespace), + RouterService: fmt.Sprintf("%s/%s", kthenaNamespace, kthenaRouterService), + } + for _, resource := range resources { + switch resource.Kind { + case kthenaKindModelRoute: + access.RouteName = resource.Name + case kthenaKindModelServer: + access.ServerName = resource.Name + } + } + if mgr.kubeClient != nil { + if svc, err := mgr.kubeClient.CoreV1().Services(kthenaNamespace).Get(ctx, kthenaRouterService, metav1.GetOptions{}); err == nil { + access.NodePortURL = nodePortURL(svc) + } + } + return access +} + +func aggregateInferencePhase(conditions []map[string]any, resources []KthenaResource) string { + if conditionPhase(conditions) == kthenaPhaseReady { + return kthenaPhaseReady + } + hasRelated := false + for _, resource := range resources { + if resource.Kind == kthenaKindModelBooster { + continue + } + hasRelated = true + if !resource.Ready { + return kthenaPhaseProgressing + } + } + if hasRelated { + return kthenaPhaseReady + } + return kthenaPhasePending +} + +func runtimeAwareInferencePhase( + phase string, + resources []KthenaResource, + runtimePods []KthenaRuntimePod, +) string { + if phase != kthenaPhaseReady && phase != kthenaPhaseActive { + return phase + } + hasModelServing := false + for _, resource := range resources { + if resource.Kind == kthenaKindModelServing { + hasModelServing = true + break + } + } + if hasModelServing && !hasReadyRuntimePod(runtimePods) { + return kthenaPhaseDegraded + } + return phase +} + +func hasReadyRuntimePod(runtimePods []KthenaRuntimePod) bool { + for _, pod := range runtimePods { + if pod.Ready { + return true + } + } + return false +} + +func normalizeConditions(obj *unstructured.Unstructured) ([]map[string]any, error) { + raw, ok, err := unstructured.NestedSlice(obj.Object, "status", "conditions") + if err != nil || !ok { + return []map[string]any{}, err + } + conditions := make([]map[string]any, 0, len(raw)) + for _, item := range raw { + if condition, ok := item.(map[string]any); ok { + conditions = append(conditions, condition) + } + } + return conditions, nil +} + +func conditionPhase(conditions []map[string]any) string { + for _, condition := range conditions { + if stringValue(condition[kthenaSpecTypeKey]) == kthenaPhaseActive && stringValue(condition["status"]) == "True" { + return kthenaPhaseReady + } + } + if len(conditions) > 0 { + return kthenaPhaseProgressing + } + return kthenaPhasePending +} + +func loadAccessibleModelDataset(ctx context.Context, datasetID uint, token util.JWTMessage) (*model.Dataset, error) { + d := query.Dataset + dataset, err := d.WithContext(ctx).Where(d.ID.Eq(datasetID), d.Type.Eq(string(model.DataTypeModel))).First() + if err != nil { + return nil, bizerr.NotFound.DataBaseNotFound.New("selected platform model was not found") + } + if dataset.UserID == token.UserID { + return dataset, nil + } + ud := query.UserDataset + if _, err := ud.WithContext(ctx).Where(ud.UserID.Eq(token.UserID), ud.DatasetID.Eq(datasetID)).First(); err == nil { + return dataset, nil + } + qd := query.AccountDataset + if _, err := qd.WithContext(ctx).Where(qd.AccountID.Eq(token.AccountID), qd.DatasetID.Eq(datasetID)).First(); err == nil { + return dataset, nil + } + return nil, bizerr.Forbidden.PermissionDenied.New("you do not have permission to use the selected platform model") +} + +func datasetToKthenaModelURI(dataset *model.Dataset) string { + url := strings.TrimSpace(dataset.URL) + if strings.HasPrefix(url, "pvc://") { + return url + } + return "pvc://" + datasetModelCacheMountPath() + "/" + strings.TrimLeft(url, "/") +} + +func datasetModelCacheURI() string { + pvcName := strings.TrimSpace(config.GetConfig().Storage.PVC.ReadWriteMany) + if pvcName == "" { + return kthenaDefaultCacheURI + } + return "pvc://" + pvcName +} + +func datasetModelCacheMountPath() string { + pvcName := strings.TrimSpace(config.GetConfig().Storage.PVC.ReadWriteMany) + if pvcName == "" { + return "/tmp/cache" + } + return "/" + strings.Trim(pvcName, "/") +} + +func buildWorkerAffinity(selectors []corev1.NodeSelectorRequirement) map[string]any { + if len(selectors) == 0 { + return nil + } + return map[string]any{ + "nodeAffinity": map[string]any{ + "requiredDuringSchedulingIgnoredDuringExecution": map[string]any{ + "nodeSelectorTerms": []any{ + map[string]any{ + "matchExpressions": selectors, + }, + }, + }, + }, + } +} + +func inferServedModelName(modelURI string) string { + if strings.Contains(modelURI, "://") { + parts := strings.Split(modelURI, "://") + modelURI = parts[len(parts)-1] + } + modelURI = strings.TrimSuffix(modelURI, "/") + parts := strings.Split(modelURI, "/") + if len(parts) == 0 { + return modelURI + } + return parts[len(parts)-1] +} + +func stringValue(value any) string { + if value == nil { + return "" + } + return fmt.Sprint(value) +} + +func nonEmpty(value, fallback string) string { + value = strings.TrimSpace(value) + if value == "" { + return fallback + } + return value +} + +func int64Value(value any) int64 { + switch v := value.(type) { + case int64: + return v + case int: + return int64(v) + case int32: + return int64(v) + case intstr.IntOrString: + return int64(v.IntVal) + case float64: + return int64(v) + case string: + parsed, _ := strconv.ParseInt(v, 10, 64) + return parsed + default: + return 0 + } +} + +func servedModelFromModelBooster(obj *unstructured.Unstructured) string { + workers, _, _ := unstructured.NestedSlice(obj.Object, "spec", "backend", "workers") + if len(workers) > 0 { + if worker, ok := workers[0].(map[string]any); ok { + configMap, _, _ := unstructured.NestedStringMap(worker, "config") + if servedModel := strings.TrimSpace(configMap["served-model-name"]); servedModel != "" { + return servedModel + } + } + } + modelURI, _, _ := unstructured.NestedString(obj.Object, "spec", "backend", "modelURI") + return inferServedModelName(modelURI) +} + +func modelSourceFromObject(obj *unstructured.Unstructured) string { + source := obj.GetAnnotations()[inferenceServiceAnnotationSource] + if source == "" { + return inferenceModelSourceExternal + } + return source +} + +func platformModelIDFromObject(obj *unstructured.Unstructured) uint { + value := obj.GetAnnotations()[inferenceServiceAnnotationModelID] + parsed, _ := strconv.ParseUint(value, 10, 64) + return uint(parsed) +} + +func envSliceToMap(value any) map[string]string { + env := map[string]string{} + items, ok := value.([]any) + if !ok { + return env + } + for _, item := range items { + m, ok := item.(map[string]any) + if !ok { + continue + } + name := stringValue(m["name"]) + if name == "" { + continue + } + env[name] = stringValue(m[kthenaEnvValueKey]) + } + return env +} + +func nestedResourceValue(worker map[string]any, key string) string { + if value, _, _ := unstructured.NestedString(worker, "resources", "limits", key); value != "" { + return value + } + value, _, _ := unstructured.NestedString(worker, "resources", "requests", key) + return value +} + +func firstGPUResourceName(worker map[string]any) string { + limits, ok, _ := unstructured.NestedMap(worker, "resources", "limits") + if !ok { + return "" + } + for key := range limits { + if key != kthenaResourceCPUKey && key != kthenaResourceMemoryKey { + return key + } + } + return "" +} + +func firstGPUResourceValue(worker map[string]any) string { + name := firstGPUResourceName(worker) + if name == "" { + return "0" + } + return nestedResourceValue(worker, name) +} + +func withDefaultModel(body []byte, modelName string) ([]byte, error) { + if len(bytes.TrimSpace(body)) == 0 || strings.TrimSpace(modelName) == "" { + return body, nil + } + var payload map[string]any + if err := json.Unmarshal(body, &payload); err != nil { + return nil, bizerr.BadRequest.InvalidRequest.New("request body must be valid JSON") + } + if strings.TrimSpace(stringValue(payload["model"])) == "" { + payload["model"] = modelName + } + return json.Marshal(payload) +} + +func sanitizeKubeName(value string) string { + value = strings.ToLower(value) + var b strings.Builder + lastHyphen := false + for _, r := range value { + valid := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') + if valid { + b.WriteRune(r) + lastHyphen = false + continue + } + if !lastHyphen { + b.WriteByte('-') + lastHyphen = true + } + } + return strings.Trim(b.String(), "-") +} + +func nodePortURL(svc *corev1.Service) string { + if svc == nil || svc.Spec.Type != corev1.ServiceTypeLoadBalancer && svc.Spec.Type != corev1.ServiceTypeNodePort { + return "" + } + for _, port := range svc.Spec.Ports { + if port.NodePort <= 0 { + continue + } + nodeHost := firstExternalOrInternalIP(svc) + if nodeHost == "" { + return fmt.Sprintf("http://:%d/v1", port.NodePort) + } + return fmt.Sprintf("http://%s:%d/v1", nodeHost, port.NodePort) + } + return "" +} + +func firstExternalOrInternalIP(svc *corev1.Service) string { + for _, ingress := range svc.Status.LoadBalancer.Ingress { + if ingress.IP != "" { + return ingress.IP + } + if ingress.Hostname != "" { + return ingress.Hostname + } + } + return "" +} diff --git a/backend/internal/handler/inference_service_test.go b/backend/internal/handler/inference_service_test.go new file mode 100644 index 000000000..19e10022b --- /dev/null +++ b/backend/internal/handler/inference_service_test.go @@ -0,0 +1,307 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + controllerfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/bizerr" + "github.com/raids-lab/crater/internal/service" + "github.com/raids-lab/crater/internal/util" +) + +const ( + kthenaInferenceTestServiceName = "qwen-demo" + kthenaInferenceTestModelURI = "hf://Qwen/Qwen2.5-0.5B-Instruct" + kthenaInferenceTestImage = "example.com/vllm:latest" +) + +func TestValidateCreateKthenaReqV1Defaults(t *testing.T) { + t.Parallel() + + req := &CreateKthenaReq{ + Name: kthenaInferenceTestServiceName, + ModelSource: inferenceModelSourceExternal, + ModelURI: kthenaInferenceTestModelURI, + BackendType: kthenaBackendVLLM, + Worker: KthenaWorkerReq{ + Image: "public.ecr.aws/q9t5s3a7/vllm-cpu-release-repo:latest", + }, + } + + if err := validateCreateKthenaReq(context.Background(), req, util.JWTMessage{}); err != nil { + t.Fatalf("validateCreateKthenaReq() error = %v", err) + } + if req.Replicas != 1 { + t.Fatalf("Replicas = %d, want 1", req.Replicas) + } + if req.Worker.Replicas != 1 { + t.Fatalf("Worker.Replicas = %d, want 1", req.Worker.Replicas) + } + if req.Worker.Config["served-model-name"] != "Qwen2.5-0.5B-Instruct" { + t.Fatalf("served-model-name = %q", req.Worker.Config["served-model-name"]) + } +} + +func TestValidateCreateKthenaReqRejectsUnsupportedV1Backend(t *testing.T) { + t.Parallel() + + for _, backendType := range []string{"SGLang", "MindIE", "vLLMDisaggregated"} { + t.Run(backendType, func(t *testing.T) { + t.Parallel() + req := &CreateKthenaReq{ + Name: kthenaInferenceTestServiceName, + ModelSource: inferenceModelSourceExternal, + ModelURI: kthenaInferenceTestModelURI, + BackendType: backendType, + Worker: KthenaWorkerReq{ + Image: kthenaInferenceTestImage, + }, + } + + if err := validateCreateKthenaReq(context.Background(), req, util.JWTMessage{}); err == nil { + t.Fatal("validateCreateKthenaReq() error = nil, want unsupported backend error") + } + }) + } +} + +func TestBuildModelBoosterObjectUsesKthenaV1Replicas(t *testing.T) { + t.Parallel() + + req := &CreateKthenaReq{ + Name: kthenaInferenceTestServiceName, + ModelSource: inferenceModelSourceExternal, + ModelURI: kthenaInferenceTestModelURI, + BackendType: kthenaBackendVLLM, + CacheURI: kthenaDefaultCacheURI, + Replicas: 3, + Worker: KthenaWorkerReq{ + Image: kthenaInferenceTestImage, + Replicas: 1, + Pods: 1, + CPU: "2", + Memory: kthenaDefaultWorkerMemory, + Config: map[string]string{"served-model-name": kthenaConversationTestService}, + }, + } + + obj := buildModelBoosterObject(req, util.JWTMessage{ + UserID: 1, + AccountID: 2, + Username: kthenaConversationTestUsername, + }, "crater-workspace") + replicas, found, err := unstructured.NestedInt64(obj.Object, "spec", "backend", "replicas") + if err != nil || !found { + t.Fatalf("spec.backend.replicas not found: found=%t err=%v", found, err) + } + if replicas != 3 { + t.Fatalf("spec.backend.replicas = %d, want 3", replicas) + } + if _, found, _ := unstructured.NestedFieldNoCopy(obj.Object, "spec", "backend", "minReplicas"); found { + t.Fatal("legacy spec.backend.minReplicas is present") + } + if _, found, _ := unstructured.NestedFieldNoCopy(obj.Object, "spec", "backend", "maxReplicas"); found { + t.Fatal("legacy spec.backend.maxReplicas is present") + } + workers, found, err := unstructured.NestedSlice(obj.Object, "spec", "backend", "workers") + if err != nil || !found || len(workers) != 1 { + t.Fatalf("spec.backend.workers invalid: found=%t count=%d err=%v", found, len(workers), err) + } + worker, ok := workers[0].(map[string]any) + if !ok { + t.Fatalf("spec.backend.workers[0] type = %T, want map[string]any", workers[0]) + } + if _, found := worker["affinity"]; found { + t.Fatal("empty worker affinity is present") + } + if username := obj.GetAnnotations()[inferenceServiceAnnotationUsername]; username != kthenaConversationTestUsername { + t.Fatalf("creator annotation = %q, want alice", username) + } +} + +func TestKthenaServiceOwnerUsesCreatorAnnotation(t *testing.T) { + t.Parallel() + + obj := &unstructured.Unstructured{} + obj.SetAnnotations(map[string]string{ + inferenceServiceAnnotationUsername: " alice ", + }) + + owner, userInfo := kthenaServiceOwner(obj) + if owner != kthenaConversationTestUsername { + t.Fatalf("owner = %q, want alice", owner) + } + if userInfo.Username != kthenaConversationTestUsername { + t.Fatalf("userInfo.username = %q, want alice", userInfo.Username) + } + if userInfo.Nickname != "" { + t.Fatalf("userInfo.nickname = %q, want empty", userInfo.Nickname) + } +} + +func TestModelBoosterToRespIncludesOwnerUserInfo(t *testing.T) { + t.Parallel() + + req := &CreateKthenaReq{ + Name: kthenaInferenceTestServiceName, + ModelSource: inferenceModelSourceExternal, + ModelURI: kthenaInferenceTestModelURI, + BackendType: kthenaBackendVLLM, + CacheURI: kthenaDefaultCacheURI, + Replicas: 1, + Worker: KthenaWorkerReq{ + Image: kthenaInferenceTestImage, + Replicas: 1, + Pods: 1, + CPU: "2", + Memory: kthenaDefaultWorkerMemory, + Config: map[string]string{"served-model-name": kthenaConversationTestService}, + }, + } + obj := buildModelBoosterObject(req, util.JWTMessage{Username: kthenaConversationTestUsername}, "crater-workspace") + mgr := &KthenaMgr{client: controllerfake.NewClientBuilder().WithScheme(runtime.NewScheme()).Build()} + + resp, err := mgr.modelBoosterToResp(context.Background(), obj) + if err != nil { + t.Fatalf("modelBoosterToResp() error = %v", err) + } + if resp.Owner != kthenaConversationTestUsername { + t.Fatalf("response owner = %q, want alice", resp.Owner) + } + if resp.UserInfo.Username != kthenaConversationTestUsername { + t.Fatalf("response userInfo.username = %q, want alice", resp.UserInfo.Username) + } +} + +func TestRuntimeAwareInferencePhase(t *testing.T) { + t.Parallel() + + resources := []KthenaResource{{Kind: kthenaKindModelServing}} + if got := runtimeAwareInferencePhase(kthenaPhaseReady, resources, nil); got != kthenaPhaseDegraded { + t.Fatalf("runtimeAwareInferencePhase() = %q, want Degraded", got) + } + if got := runtimeAwareInferencePhase(kthenaPhaseReady, resources, []KthenaRuntimePod{{Ready: true}}); got != kthenaPhaseReady { + t.Fatalf("runtimeAwareInferencePhase() = %q, want Ready", got) + } + if got := runtimeAwareInferencePhase(kthenaPhaseProgressing, resources, nil); got != kthenaPhaseProgressing { + t.Fatalf("runtimeAwareInferencePhase() = %q, want Progressing", got) + } +} + +func TestKthenaProxyHTTPStatus(t *testing.T) { + t.Parallel() + + err := apierrors.NewNotFound(schema.GroupResource{Resource: "modelservers"}, "qwen") + if got := kthenaProxyHTTPStatus(err); got != http.StatusNotFound { + t.Fatalf("kthenaProxyHTTPStatus() = %d, want %d", got, http.StatusNotFound) + } + if got := kthenaProxyHTTPStatus(context.DeadlineExceeded); got != http.StatusBadGateway { + t.Fatalf("kthenaProxyHTTPStatus() = %d, want %d", got, http.StatusBadGateway) + } +} + +func TestDiagnosticsFromPodEventsIgnoresStalePodEvents(t *testing.T) { + t.Parallel() + + pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{ + Name: "qwen3test-backend1-0-leader-0-0", + Namespace: "crater-workspace", + UID: types.UID("current-pod"), + }} + staleEvent := &corev1.Event{ + ObjectMeta: metav1.ObjectMeta{Name: "stale-warning", Namespace: pod.Namespace}, + InvolvedObject: corev1.ObjectReference{ + Name: pod.Name, + UID: types.UID("previous-pod"), + }, + Type: corev1.EventTypeWarning, + Reason: "BackOff", + Message: "Back-off restarting failed container engine", + } + currentEvent := &corev1.Event{ + ObjectMeta: metav1.ObjectMeta{Name: "current-warning", Namespace: pod.Namespace}, + InvolvedObject: corev1.ObjectReference{ + Name: pod.Name, + UID: pod.UID, + }, + Type: corev1.EventTypeWarning, + Reason: "FailedScheduling", + Message: "temporary scheduling warning", + } + mgr := &KthenaMgr{kubeClient: fake.NewSimpleClientset(staleEvent, currentEvent)} + + diagnostics := mgr.diagnosticsFromPodEvents(context.Background(), pod) + if len(diagnostics) != 1 { + t.Fatalf("diagnostics count = %d, want 1", len(diagnostics)) + } + if diagnostics[0].Reason != currentEvent.Reason { + t.Fatalf("diagnostic reason = %q, want %q", diagnostics[0].Reason, currentEvent.Reason) + } +} + +func TestKthenaInferenceRoutesRejectRequestsWhenFeatureIsDisabled(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_inference_routes_disabled?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}); err != nil { + t.Fatal(err) + } + + mgr := &KthenaMgr{configService: service.NewConfigService(query.Use(db))} + router := gin.New() + mgr.RegisterProtected(router.Group("/v1/kthena")) + mgr.RegisterAdmin(router.Group("/v1/admin/kthena")) + + requests := []struct { + method string + path string + }{ + {http.MethodGet, "/v1/kthena/inference-services"}, + {http.MethodPost, "/v1/kthena/inference-services"}, + {http.MethodGet, "/v1/kthena/inference-services/qwen"}, + {http.MethodGet, "/v1/kthena/inference-services/qwen/yaml"}, + {http.MethodDelete, "/v1/kthena/inference-services/qwen"}, + {http.MethodPost, "/v1/kthena/inference-services/qwen/openai/v1/chat/completions"}, + {http.MethodGet, "/v1/admin/kthena/inference-services"}, + {http.MethodGet, "/v1/admin/kthena/inference-services/qwen"}, + {http.MethodGet, "/v1/admin/kthena/inference-services/qwen/yaml"}, + {http.MethodDelete, "/v1/admin/kthena/inference-services/qwen"}, + } + for _, item := range requests { + t.Run(item.method+" "+item.path, func(t *testing.T) { + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequestWithContext(context.Background(), item.method, item.path, http.NoBody)) + if recorder.Code != http.StatusConflict { + t.Fatalf("returned HTTP %d: %s", recorder.Code, recorder.Body.String()) + } + var response struct { + Code bizerr.BizCode `json:"code"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + if response.Code != bizerr.Conflict.ResourceStatusError { + t.Fatalf("response code = %d, want %d", response.Code, bizerr.Conflict.ResourceStatusError) + } + }) + } +} diff --git a/backend/internal/handler/kthena_inference_template.go b/backend/internal/handler/kthena_inference_template.go new file mode 100644 index 000000000..480b8ec6f --- /dev/null +++ b/backend/internal/handler/kthena_inference_template.go @@ -0,0 +1,377 @@ +package handler + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + "unicode/utf8" + + "github.com/gin-gonic/gin" + "gorm.io/datatypes" + "gorm.io/gorm" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/bizerr" + "github.com/raids-lab/crater/internal/resputil" + "github.com/raids-lab/crater/internal/service" + "github.com/raids-lab/crater/internal/util" +) + +const ( + maxKthenaInferenceTemplates = 50 + maxKthenaInferenceTemplateNameRunes = 64 + maxKthenaInferenceTemplateDescRunes = 512 + maxKthenaInferenceTemplateConfigSize = 64 * 1024 +) + +//nolint:gochecknoinits // This is the standard way to register a gin handler. +func init() { + Registers = append(Registers, NewKthenaInferenceTemplateMgr) +} + +// KthenaInferenceTemplateMgr owns private, account-scoped deployment presets. +// It intentionally does not share the legacy global job-template implementation: +// those templates are public, whereas inference presets must never cross user or +// account boundaries. +type KthenaInferenceTemplateMgr struct { + name string + db *gorm.DB + configService *service.ConfigService +} + +func NewKthenaInferenceTemplateMgr(conf *RegisterConfig) Manager { + return &KthenaInferenceTemplateMgr{ + // Route managers are mounted below GetName(). Sharing the Kthena root + // keeps private templates at /v1/kthena/inference-templates rather than + // exposing an unrelated top-level API namespace. + name: "kthena", + db: query.GetDB(), + configService: conf.ConfigService, + } +} + +func (mgr *KthenaInferenceTemplateMgr) GetName() string { return mgr.name } + +func (mgr *KthenaInferenceTemplateMgr) RegisterPublic(_ *gin.RouterGroup) {} + +func (mgr *KthenaInferenceTemplateMgr) RegisterProtected(g *gin.RouterGroup) { + templates := g.Group("inference-templates", mgr.requireKthenaInferenceEnabled) + templates.GET("", mgr.ListKthenaInferenceTemplates) + templates.POST("", mgr.CreateKthenaInferenceTemplate) + templates.PUT(":id", mgr.UpdateKthenaInferenceTemplate) + templates.DELETE(":id", mgr.DeleteKthenaInferenceTemplate) +} + +func (mgr *KthenaInferenceTemplateMgr) RegisterAdmin(_ *gin.RouterGroup) {} + +func (mgr *KthenaInferenceTemplateMgr) requireKthenaInferenceEnabled(c *gin.Context) { + if mgr.configService == nil || !mgr.configService.IsKthenaInferenceEnabled(c.Request.Context()) { + resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New("Kthena inference feature is disabled")) + c.Abort() + return + } + c.Next() +} + +// KthenaInferenceTemplateReq keeps the complete reusable form payload in a +// version-tolerant JSON object. The create-deployment endpoint remains the +// validation authority when the user applies a saved template. +type KthenaInferenceTemplateReq struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Config json.RawMessage `json:"config" binding:"required" swaggertype:"object"` +} + +type kthenaInferenceTemplateIDReq struct { + ID uint `uri:"id" binding:"required"` +} + +type KthenaInferenceTemplateResp struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Config json.RawMessage `json:"config" swaggertype:"object"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +// ListKthenaInferenceTemplates godoc +// +// @Summary List private Kthena deployment templates +// @Description List the current user's templates in the active account. Templates are never shared across users or accounts. +// @Tags kthena +// @Produce json +// @Security Bearer +// @Success 200 {object} resputil.Response[[]KthenaInferenceTemplateResp] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-templates [get] +func (mgr *KthenaInferenceTemplateMgr) ListKthenaInferenceTemplates(c *gin.Context) { + if !mgr.templateDBReady(c) { + return + } + token := util.GetToken(c) + var templates []model.KthenaInferenceTemplate + if err := mgr.db.WithContext(c.Request.Context()). + Where("user_id = ? AND account_id = ?", token.UserID, token.AccountID). + Order("updated_at DESC, id DESC"). + Limit(maxKthenaInferenceTemplates). + Find(&templates).Error; err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "list Kthena inference templates failed")) + return + } + + response := make([]KthenaInferenceTemplateResp, 0, len(templates)) + for index := range templates { + response = append(response, kthenaInferenceTemplateToResp(&templates[index])) + } + resputil.Success(c, response) +} + +// CreateKthenaInferenceTemplate godoc +// +// @Summary Create a private Kthena deployment template +// @Description Save the current deployment form as a private template for the current user and account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body KthenaInferenceTemplateReq true "Template" +// @Success 200 {object} resputil.Response[KthenaInferenceTemplateResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 409 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-templates [post] +func (mgr *KthenaInferenceTemplateMgr) CreateKthenaInferenceTemplate(c *gin.Context) { + if !mgr.templateDBReady(c) { + return + } + var req KthenaInferenceTemplateReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid Kthena inference template request")) + return + } + if err := validateKthenaInferenceTemplateReq(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + + token := util.GetToken(c) + var count int64 + if err := mgr.db.WithContext(c.Request.Context()).Model(&model.KthenaInferenceTemplate{}). + Where("user_id = ? AND account_id = ?", token.UserID, token.AccountID). + Count(&count).Error; err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "count Kthena inference templates failed")) + return + } + if count >= maxKthenaInferenceTemplates { + resputil.HandleError(c, bizerr.Conflict.ResourceStatusError.New("Kthena inference template limit reached")) + return + } + + var existing model.KthenaInferenceTemplate + err := mgr.db.WithContext(c.Request.Context()). + Where("user_id = ? AND account_id = ? AND name = ?", token.UserID, token.AccountID, req.Name). + First(&existing).Error + if err == nil { + resputil.HandleError(c, bizerr.Conflict.ResourceAlreadyExists.New("Kthena inference template name already exists")) + return + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "check Kthena inference template name failed")) + return + } + + template := model.KthenaInferenceTemplate{ + UserID: token.UserID, + AccountID: token.AccountID, + Name: req.Name, + Description: req.Description, + Config: datatypes.JSON(append([]byte(nil), req.Config...)), + } + if err := mgr.db.WithContext(c.Request.Context()).Create(&template).Error; err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "create Kthena inference template failed")) + return + } + resputil.Success(c, kthenaInferenceTemplateToResp(&template)) +} + +// UpdateKthenaInferenceTemplate godoc +// +// @Summary Update a private Kthena deployment template +// @Description Replace a template owned by the current user in the active account. +// @Tags kthena +// @Accept json +// @Produce json +// @Security Bearer +// @Param id path int true "Template ID" +// @Param request body KthenaInferenceTemplateReq true "Template" +// @Success 200 {object} resputil.Response[KthenaInferenceTemplateResp] +// @Failure 400 {object} resputil.Response[any] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-templates/{id} [put] +func (mgr *KthenaInferenceTemplateMgr) UpdateKthenaInferenceTemplate(c *gin.Context) { + if !mgr.templateDBReady(c) { + return + } + var idReq kthenaInferenceTemplateIDReq + if err := c.ShouldBindUri(&idReq); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid Kthena inference template id")) + return + } + var req KthenaInferenceTemplateReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.InvalidRequest.Wrap(err, "invalid Kthena inference template request")) + return + } + if err := validateKthenaInferenceTemplateReq(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, err.Error())) + return + } + + token := util.GetToken(c) + template, ok := mgr.findPrivateTemplate(c, idReq.ID, token.UserID, token.AccountID) + if !ok { + return + } + if req.Name != template.Name { + var duplicate model.KthenaInferenceTemplate + err := mgr.db.WithContext(c.Request.Context()). + Where("user_id = ? AND account_id = ? AND name = ?", token.UserID, token.AccountID, req.Name). + First(&duplicate).Error + if err == nil { + resputil.HandleError(c, bizerr.Conflict.ResourceAlreadyExists.New("Kthena inference template name already exists")) + return + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "check Kthena inference template name failed")) + return + } + } + + template.Name = req.Name + template.Description = req.Description + template.Config = datatypes.JSON(append([]byte(nil), req.Config...)) + if err := mgr.db.WithContext(c.Request.Context()).Save(template).Error; err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "update Kthena inference template failed")) + return + } + resputil.Success(c, kthenaInferenceTemplateToResp(template)) +} + +// DeleteKthenaInferenceTemplate godoc +// +// @Summary Delete a private Kthena deployment template +// @Description Delete one template owned by the current user in the active account. +// @Tags kthena +// @Produce json +// @Security Bearer +// @Param id path int true "Template ID" +// @Success 200 {object} resputil.Response[string] +// @Failure 404 {object} resputil.Response[any] +// @Failure 500 {object} resputil.Response[any] +// @Router /v1/kthena/inference-templates/{id} [delete] +func (mgr *KthenaInferenceTemplateMgr) DeleteKthenaInferenceTemplate(c *gin.Context) { + if !mgr.templateDBReady(c) { + return + } + var idReq kthenaInferenceTemplateIDReq + if err := c.ShouldBindUri(&idReq); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid Kthena inference template id")) + return + } + token := util.GetToken(c) + template, ok := mgr.findPrivateTemplate(c, idReq.ID, token.UserID, token.AccountID) + if !ok { + return + } + if err := mgr.db.WithContext(c.Request.Context()).Delete(template).Error; err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "delete Kthena inference template failed")) + return + } + resputil.Success(c, "Kthena inference template deleted") +} + +func (mgr *KthenaInferenceTemplateMgr) templateDBReady(c *gin.Context) bool { + if mgr.db != nil { + return true + } + resputil.HandleError(c, bizerr.Internal.DatabaseError.New("Kthena inference template storage is not initialized")) + return false +} + +func (mgr *KthenaInferenceTemplateMgr) findPrivateTemplate( + c *gin.Context, + id uint, + userID uint, + accountID uint, +) (*model.KthenaInferenceTemplate, bool) { + template := &model.KthenaInferenceTemplate{} + err := mgr.db.WithContext(c.Request.Context()). + Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID). + First(template).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + resputil.HandleError(c, bizerr.NotFound.DataBaseNotFound.New("Kthena inference template not found")) + return nil, false + } + if err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "find Kthena inference template failed")) + return nil, false + } + return template, true +} + +func validateKthenaInferenceTemplateReq(req *KthenaInferenceTemplateReq) error { + if req == nil { + return bizerr.BadRequest.MissingParameter.New("kthena inference template is required") + } + req.Name = strings.TrimSpace(req.Name) + req.Description = strings.TrimSpace(req.Description) + if req.Name == "" || utf8.RuneCountInString(req.Name) > maxKthenaInferenceTemplateNameRunes { + return bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("template name must contain 1 to %d characters", maxKthenaInferenceTemplateNameRunes), + ) + } + if utf8.RuneCountInString(req.Description) > maxKthenaInferenceTemplateDescRunes { + return bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("template description cannot exceed %d characters", maxKthenaInferenceTemplateDescRunes), + ) + } + if len(req.Config) == 0 || len(req.Config) > maxKthenaInferenceTemplateConfigSize { + return bizerr.BadRequest.ParameterError.New( + fmt.Sprintf("template config must contain at most %d bytes", maxKthenaInferenceTemplateConfigSize), + ) + } + var config map[string]json.RawMessage + if err := json.Unmarshal(req.Config, &config); err != nil || config == nil { + return bizerr.BadRequest.InvalidRequest.New("template config must be a JSON object") + } + backendRaw, ok := config["backendType"] + if !ok { + return bizerr.BadRequest.MissingParameter.New("template config must include backendType") + } + var backendType string + if err := json.Unmarshal(backendRaw, &backendType); err != nil || strings.TrimSpace(backendType) != kthenaBackendVLLM { + return bizerr.BadRequest.ParameterError.New("template config backendType must be vLLM") + } + return nil +} + +func kthenaInferenceTemplateToResp( + template *model.KthenaInferenceTemplate, +) KthenaInferenceTemplateResp { + if template == nil { + return KthenaInferenceTemplateResp{} + } + return KthenaInferenceTemplateResp{ + ID: template.ID, + Name: template.Name, + Description: template.Description, + Config: json.RawMessage(append([]byte(nil), template.Config...)), + CreatedAt: template.CreatedAt, + UpdatedAt: template.UpdatedAt, + } +} diff --git a/backend/internal/handler/kthena_inference_template_test.go b/backend/internal/handler/kthena_inference_template_test.go new file mode 100644 index 000000000..47d7027be --- /dev/null +++ b/backend/internal/handler/kthena_inference_template_test.go @@ -0,0 +1,161 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/service" + "github.com/raids-lab/crater/internal/util" +) + +func TestKthenaInferenceTemplateHandlersArePrivateToCurrentUserAndAccount(t *testing.T) { + gin.SetMode(gin.TestMode) + db, err := gorm.Open(sqlite.Open("file:kthena_inference_template_handlers?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}, &model.KthenaInferenceTemplate{}); err != nil { + t.Fatal(err) + } + configService := service.NewConfigService(query.Use(db)) + if err := configService.SetKthenaInferenceEnabled(t.Context(), true); err != nil { + t.Fatal(err) + } + + currentToken := util.JWTMessage{UserID: 11, AccountID: 22, Username: "template-owner"} + manager := &KthenaInferenceTemplateMgr{db: db, configService: configService} + router := gin.New() + router.Use(func(c *gin.Context) { + util.SetJWTContext(c, currentToken) + c.Next() + }) + manager.RegisterProtected(router.Group("/v1/kthena")) + + created := requestKthenaInferenceTemplate(t, router, http.MethodPost, "/v1/kthena/inference-templates", `{ + "name":"my V100 template", + "description":"private preset", + "config":{"backendType":"vLLM","resource":{"gpu":{"count":1,"model":"nvidia.com/gpu"}}} + }`) + if created.ID == 0 || created.Name != "my V100 template" || kthenaInferenceTemplateConfig(t, &created).BackendType != "vLLM" { + t.Fatalf("created template = %+v", created) + } + + listed := requestKthenaInferenceTemplateList(t, router) + if len(listed) != 1 || listed[0].ID != created.ID { + t.Fatalf("owner list = %+v", listed) + } + + // A different identity in the same browser/server process cannot read or modify it. + currentToken = util.JWTMessage{UserID: 12, AccountID: 22, Username: "other-user"} + if listed = requestKthenaInferenceTemplateList(t, router); len(listed) != 0 { + t.Fatalf("foreign user received templates: %+v", listed) + } + response := httptest.NewRecorder() + request := httptest.NewRequestWithContext(t.Context(), http.MethodDelete, + "/v1/kthena/inference-templates/"+formatKthenaInferenceTemplateID(created.ID), http.NoBody) + // The endpoint must return not found for another user instead of disclosing ownership. + router.ServeHTTP(response, request) + if response.Code != http.StatusNotFound { + t.Fatalf("foreign delete returned %d: %s", response.Code, response.Body.String()) + } + + currentToken = util.JWTMessage{UserID: 11, AccountID: 22, Username: "template-owner"} + updated := requestKthenaInferenceTemplate(t, router, http.MethodPut, + "/v1/kthena/inference-templates/"+formatKthenaInferenceTemplateID(created.ID), `{ + "name":"my V100 template", + "description":"updated private preset", + "config":{"backendType":"vLLM","replicas":2} + }`) + if updated.Description != "updated private preset" || kthenaInferenceTemplateConfig(t, &updated).Replicas != 2 { + t.Fatalf("updated template = %+v", updated) + } +} + +func TestValidateKthenaInferenceTemplateReq(t *testing.T) { + valid := &KthenaInferenceTemplateReq{ + Name: " Private vLLM ", + Config: json.RawMessage(`{"backendType":"vLLM"}`), + } + if err := validateKthenaInferenceTemplateReq(valid); err != nil { + t.Fatalf("valid request: %v", err) + } + if valid.Name != "Private vLLM" { + t.Fatalf("trimmed name = %q", valid.Name) + } + for _, config := range []json.RawMessage{ + json.RawMessage(`[]`), + json.RawMessage(`{"backendType":"SGLang"}`), + json.RawMessage(`{"backendType":123}`), + } { + if err := validateKthenaInferenceTemplateReq(&KthenaInferenceTemplateReq{Name: "invalid", Config: config}); err == nil { + t.Fatalf("config %s unexpectedly passed validation", config) + } + } +} + +func requestKthenaInferenceTemplate( + t *testing.T, router http.Handler, method, path, body string, +) KthenaInferenceTemplateResp { + t.Helper() + response := httptest.NewRecorder() + request := httptest.NewRequestWithContext(t.Context(), method, path, bytes.NewBufferString(body)) + request.Header.Set("Content-Type", "application/json") + router.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("%s %s returned %d: %s", method, path, response.Code, response.Body.String()) + } + var payload struct { + Data KthenaInferenceTemplateResp `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + return payload.Data +} + +func requestKthenaInferenceTemplateList(t *testing.T, router http.Handler) []KthenaInferenceTemplateResp { + t.Helper() + response := httptest.NewRecorder() + router.ServeHTTP(response, httptest.NewRequestWithContext( + t.Context(), http.MethodGet, "/v1/kthena/inference-templates", http.NoBody, + )) + if response.Code != http.StatusOK { + t.Fatalf("GET templates returned %d: %s", response.Code, response.Body.String()) + } + var payload struct { + Data []KthenaInferenceTemplateResp `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { + t.Fatal(err) + } + return payload.Data +} + +func formatKthenaInferenceTemplateID(id uint) string { + return strconv.FormatUint(uint64(id), 10) +} + +func kthenaInferenceTemplateConfig(t *testing.T, template *KthenaInferenceTemplateResp) struct { + BackendType string `json:"backendType"` + Replicas int `json:"replicas"` +} { + t.Helper() + var config struct { + BackendType string `json:"backendType"` + Replicas int `json:"replicas"` + } + if err := json.Unmarshal(template.Config, &config); err != nil { + t.Fatal(err) + } + return config +} diff --git a/backend/internal/handler/system_config.go b/backend/internal/handler/system_config.go index b0e9173fc..48afaa9dc 100644 --- a/backend/internal/handler/system_config.go +++ b/backend/internal/handler/system_config.go @@ -52,6 +52,7 @@ func (mgr *SystemConfigMgr) RegisterPublic(_ *gin.RouterGroup) {} func (mgr *SystemConfigMgr) RegisterProtected(g *gin.RouterGroup) { g.GET("/billing", mgr.GetBillingStatus) g.GET("/model-download-limit", mgr.GetModelDownloadLimitConfig) + g.GET("/kthena-inference", mgr.GetKthenaInferenceStatus) } func (mgr *SystemConfigMgr) RegisterAdmin(g *gin.RouterGroup) { @@ -63,6 +64,8 @@ func (mgr *SystemConfigMgr) RegisterAdmin(g *gin.RouterGroup) { g.GET("/gpu-analysis", mgr.GetGpuAnalysisStatus) g.PUT("/gpu-analysis", mgr.SetGpuAnalysisStatus) + g.GET("/kthena-inference", mgr.GetKthenaInferenceStatus) + g.PUT("/kthena-inference", mgr.SetKthenaInferenceStatus) g.GET("/prequeue", mgr.GetPrequeueConfig) g.PUT("/prequeue", mgr.UpdatePrequeueConfig) g.GET("/model-download-limit", mgr.GetAdminModelDownloadLimitConfig) @@ -100,6 +103,14 @@ type SetGpuAnalysisStatusReq struct { Enable bool `json:"enable"` } +type KthenaInferenceStatusResp struct { + Enabled bool `json:"enabled"` +} + +type SetKthenaInferenceStatusReq struct { + Enabled *bool `json:"enabled" binding:"required"` +} + type PrequeueConfigResp struct { BackfillEnabled bool `json:"backfillEnabled"` QueueQuotaEnabled bool `json:"queueQuotaEnabled"` @@ -336,6 +347,50 @@ func (mgr *SystemConfigMgr) SetGpuAnalysisStatus(c *gin.Context) { resputil.Success(c, "GPU analysis "+action) } +// GetKthenaInferenceStatus godoc +// @Summary 获取模型部署功能开关状态 +// @Description 查询当前系统是否允许用户创建和管理基于 Kthena 的在线模型部署。 +// @Tags SystemConfig +// @Produce json +// @Security Bearer +// @Success 200 {object} resputil.Response[KthenaInferenceStatusResp] "开关状态" +// @Router /v1/system-config/kthena-inference [get] +// @Router /v1/admin/system-config/kthena-inference [get] +func (mgr *SystemConfigMgr) GetKthenaInferenceStatus(c *gin.Context) { + resputil.Success(c, KthenaInferenceStatusResp{ + Enabled: mgr.service.IsKthenaInferenceEnabled(c.Request.Context()), + }) +} + +// SetKthenaInferenceStatus godoc +// @Summary 设置模型部署功能开关 +// @Description 开启后,用户可以创建和管理基于 Kthena 的在线模型部署;关闭后所有模型部署接口均会拒绝访问。 +// @Tags SystemConfig +// @Accept json +// @Produce json +// @Security Bearer +// @Param data body SetKthenaInferenceStatusReq true "开关设置" +// @Success 200 {object} resputil.Response[string] "设置成功" +// @Failure 400 {object} resputil.Response[any] "请求参数错误" +// @Router /v1/admin/system-config/kthena-inference [put] +func (mgr *SystemConfigMgr) SetKthenaInferenceStatus(c *gin.Context) { + var req SetKthenaInferenceStatusReq + if err := c.ShouldBindJSON(&req); err != nil { + resputil.HandleError(c, bizerr.BadRequest.ParameterError.Wrap(err, "invalid Kthena inference feature config")) + return + } + if err := mgr.service.SetKthenaInferenceEnabled(c.Request.Context(), *req.Enabled); err != nil { + resputil.HandleError(c, bizerr.Internal.DatabaseError.Wrap(err, "update Kthena inference feature config failed")) + return + } + + action := "disabled" + if *req.Enabled { + action = "enabled" + } + resputil.Success(c, "Kthena inference feature "+action) +} + // GetPrequeueConfig godoc // @Summary 获取新版排队配置 // @Description 获取当前回填提交开关、Crater 队内资源配额开关、普通作业等待忍耐时间和 watcher 运行参数 diff --git a/backend/internal/handler/system_config_test.go b/backend/internal/handler/system_config_test.go index e942c1b99..8e3ee48cf 100644 --- a/backend/internal/handler/system_config_test.go +++ b/backend/internal/handler/system_config_test.go @@ -122,6 +122,67 @@ func TestModelDownloadLimitConfigRoutesHideWhitelistFromProtectedUsers(t *testin } } +func TestKthenaInferenceStatusRoutes(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_inference_status_handlers?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}); err != nil { + t.Fatal(err) + } + configService := service.NewConfigService(query.Use(db)) + mgr := &SystemConfigMgr{service: configService} + router := gin.New() + mgr.RegisterProtected(router.Group("/v1/system-config")) + mgr.RegisterAdmin(router.Group("/v1/admin/system-config")) + + assertKthenaInferenceStatus(t, router, "/v1/system-config/kthena-inference", false) + assertKthenaInferenceStatus(t, router, "/v1/admin/system-config/kthena-inference", false) + setKthenaInferenceStatus(t, router, true) + assertKthenaInferenceStatus(t, router, "/v1/system-config/kthena-inference", true) + setKthenaInferenceStatus(t, router, false) + assertKthenaInferenceStatus(t, router, "/v1/admin/system-config/kthena-inference", false) +} + +func assertKthenaInferenceStatus(t *testing.T, router http.Handler, path string, wantEnabled bool) { + t.Helper() + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, path, http.NoBody) + router.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("GET %s returned HTTP %d: %s", path, recorder.Code, recorder.Body.String()) + } + + var response struct { + Data KthenaInferenceStatusResp `json:"data"` + } + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + if response.Data.Enabled != wantEnabled { + t.Fatalf("GET %s enabled = %t, want %t", path, response.Data.Enabled, wantEnabled) + } +} + +func setKthenaInferenceStatus(t *testing.T, router http.Handler, enabled bool) { + t.Helper() + recorder := httptest.NewRecorder() + body := `{"enabled":false}` + if enabled { + body = `{"enabled":true}` + } + request := httptest.NewRequest( + http.MethodPut, + "/v1/admin/system-config/kthena-inference", + bytes.NewBufferString(body), + ) + request.Header.Set("Content-Type", "application/json") + router.ServeHTTP(recorder, request) + if recorder.Code != http.StatusOK { + t.Fatalf("PUT returned HTTP %d: %s", recorder.Code, recorder.Body.String()) + } +} + func requestModelDownloadLimitConfig( t *testing.T, router http.Handler, path string, ) map[string]json.RawMessage { diff --git a/backend/internal/handler/vcjob/list.go b/backend/internal/handler/vcjob/list.go index 05874ed07..fe039e0f4 100644 --- a/backend/internal/handler/vcjob/list.go +++ b/backend/internal/handler/vcjob/list.go @@ -31,12 +31,16 @@ type jobListScope struct { } type jobListQuery struct { - Page int `form:"page,default=1" binding:"min=1"` - PageSize int `form:"page_size,default=10" binding:"min=1,max=200"` - Sort string `form:"sort"` - Search string `form:"search"` - Days *int `form:"days" binding:"omitempty,eq=-1|gt=0"` - JobTypes []string `form:"job_type" binding:"max=20,dive,required"` + Page int `form:"page,default=1" binding:"min=1"` + PageSize int `form:"page_size,default=10" binding:"min=1,max=200"` + Sort string `form:"sort"` + Search string `form:"search"` + Days *int `form:"days" binding:"omitempty,eq=-1|gt=0"` + JobTypes []string `form:"job_type" binding:"max=20,dive,required"` + // WorkloadKinds is only consumed by the unified workload list. Keeping it + // in the shared query makes the new endpoint accept the same filters as the + // existing job list without changing the latter's response contract. + WorkloadKinds []string `form:"workload_kind" binding:"max=20,dive,required"` ScheduleTypes []int `form:"schedule_type" binding:"max=20,dive,oneof=0 1"` Statuses []string `form:"status" binding:"max=20,dive,required"` Node *string `form:"node"` @@ -125,11 +129,18 @@ func parseJobSort(raw string) ([]jobSort, error) { func validateJobListEnums(request *jobListQuery) error { for _, value := range request.JobTypes { switch value { - case "jupyter", "webide", "pytorch", "tensorflow", "kuberay", "deepspeed", "openmpi", "custom": + case "jupyter", "webide", "pytorch", "tensorflow", "kuberay", "deepspeed", "openmpi", "custom", "model-deployment": default: return bizerr.BadRequest.ParameterError.New("unsupported job_type " + strconv.Quote(value)) } } + for _, value := range request.WorkloadKinds { + switch value { + case workloadKindVolcanoJob, workloadKindKthenaInference: + default: + return bizerr.BadRequest.ParameterError.New("unsupported workload_kind " + strconv.Quote(value)) + } + } for _, value := range request.Statuses { switch value { case "Prequeue", "Pending", "Aborting", "Aborted", "Running", "Restarting", "Completing", @@ -281,6 +292,8 @@ func jobFacetQuery(source *jobListQuery, facet string) jobListQuery { request.ScheduleTypes = nil case "status": request.Statuses = nil + case "workload_kind": + request.WorkloadKinds = nil } return request } diff --git a/backend/internal/handler/vcjob/vcjob.go b/backend/internal/handler/vcjob/vcjob.go index 5404347b0..0e43f12d5 100644 --- a/backend/internal/handler/vcjob/vcjob.go +++ b/backend/internal/handler/vcjob/vcjob.go @@ -45,34 +45,36 @@ func init() { } type VolcanojobMgr struct { - name string - client client.Client - config *rest.Config - kubeClient kubernetes.Interface - imagePacker packer.ImagePackerInterface - imageRegistry imageregistry.ImageRegistryInterface - serviceManager crclient.ServiceManagerInterface - configService *service.ConfigService - queueQuotaSvc *service.PrequeueService - prequeueWatcher *prequeuewatcher.PrequeueWatcher - billingService *service.BillingService - userBanService *service.UserBanService + name string + client client.Client + workloadNamespace string + config *rest.Config + kubeClient kubernetes.Interface + imagePacker packer.ImagePackerInterface + imageRegistry imageregistry.ImageRegistryInterface + serviceManager crclient.ServiceManagerInterface + configService *service.ConfigService + queueQuotaSvc *service.PrequeueService + prequeueWatcher *prequeuewatcher.PrequeueWatcher + billingService *service.BillingService + userBanService *service.UserBanService } func NewVolcanojobMgr(conf *handler.RegisterConfig) handler.Manager { return &VolcanojobMgr{ - name: "vcjobs", - client: conf.Client, - config: conf.KubeConfig, - kubeClient: conf.KubeClient, - imagePacker: conf.ImagePacker, - imageRegistry: conf.ImageRegistry, - serviceManager: conf.ServiceManager, - configService: conf.ConfigService, - queueQuotaSvc: conf.PrequeueService, - prequeueWatcher: conf.PrequeueWatcher, - billingService: conf.BillingService, - userBanService: conf.UserBanService, + name: "vcjobs", + client: conf.Client, + workloadNamespace: config.GetConfig().Namespaces.Job, + config: conf.KubeConfig, + kubeClient: conf.KubeClient, + imagePacker: conf.ImagePacker, + imageRegistry: conf.ImageRegistry, + serviceManager: conf.ServiceManager, + configService: conf.ConfigService, + queueQuotaSvc: conf.PrequeueService, + prequeueWatcher: conf.PrequeueWatcher, + billingService: conf.BillingService, + userBanService: conf.UserBanService, } } @@ -83,6 +85,15 @@ func (mgr *VolcanojobMgr) RegisterPublic(_ *gin.RouterGroup) {} func (mgr *VolcanojobMgr) RegisterProtected(g *gin.RouterGroup) { g.GET("", mgr.GetSelfJobs) g.GET("facets", mgr.GetSelfJobFacets) + // The unified workload endpoints intentionally sit alongside the legacy + // Volcano job APIs. They return database-backed Volcano jobs and Kthena + // ModelBoosters without turning an inference deployment into a Job record. + g.GET("workloads", mgr.GetSelfWorkloads) + g.GET("workloads/facets", mgr.GetSelfWorkloadFacets) + g.GET("workloads/all", mgr.GetAllWorkloads) + g.GET("workloads/all/facets", mgr.GetAllWorkloadFacets) + g.GET("workloads/user/:username", mgr.GetUserWorkloads) + g.GET("workloads/user/:username/facets", mgr.GetUserWorkloadFacets) g.GET("all", mgr.GetAllJobsInDays) g.GET("all/facets", mgr.GetAllJobFacets) g.GET("user/:username", mgr.GetUserJobsInDays) @@ -127,6 +138,8 @@ func (mgr *VolcanojobMgr) RegisterProtected(g *gin.RouterGroup) { func (mgr *VolcanojobMgr) RegisterAdmin(g *gin.RouterGroup) { g.GET("", mgr.GetAllJobsInDays) g.GET("facets", mgr.GetAllJobFacets) + g.GET("workloads", mgr.GetAllWorkloads) + g.GET("workloads/facets", mgr.GetAllWorkloadFacets) g.GET("user/:username", mgr.GetUserJobsInDays) g.GET("user/:username/facets", mgr.GetUserJobFacets) g.GET("billing", mgr.GetAllJobBillingInDays) diff --git a/backend/internal/handler/vcjob/workload.go b/backend/internal/handler/vcjob/workload.go new file mode 100644 index 000000000..25ced288d --- /dev/null +++ b/backend/internal/handler/vcjob/workload.go @@ -0,0 +1,715 @@ +package vcjob + +import ( + "context" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/raids-lab/crater/dao/model" + "github.com/raids-lab/crater/dao/query" + "github.com/raids-lab/crater/internal/bizerr" + "github.com/raids-lab/crater/internal/resputil" + "github.com/raids-lab/crater/internal/util" + "github.com/raids-lab/crater/pkg/config" +) + +const ( + workloadKindVolcanoJob = "volcano-job" + workloadKindKthenaInference = "kthena-inference" + workloadJobTypeModelDeploy = "model-deployment" + workloadKthenaManagedByLabel = "crater.raids.io/managed-by" + workloadKthenaManagedByValue = "inference-service" + workloadKthenaUserIDLabel = "crater.raids.io/user-id" + workloadKthenaAccountIDLabel = "crater.raids.io/account-id" + workloadKthenaUserAnnotation = "crater.raids.io/user" + workloadKthenaAccountAnno = "crater.raids.io/account" + + workloadStatusPending = "Pending" + workloadStatusRunning = "Running" + workloadStatusCompleted = "Completed" + workloadStatusFailed = "Failed" + workloadPhaseReady = "Ready" + workloadConditionActive = "Active" + workloadConditionTrue = "True" + + workloadSortID = "id" + workloadSortName = "name" + workloadSortJobName = "jobName" + workloadSortOwner = "owner" + workloadSortQueue = "queue" + workloadSortJobType = "jobType" + workloadSortScheduleType = "scheduleType" + workloadFieldStatus = "status" + workloadSortBilledPointsTotal = "billedPointsTotal" + workloadSortCreatedAt = "createdAt" + workloadSortStartedAt = "startedAt" + workloadSortCompletedAt = "completedAt" + + workloadFacetJobType = "job_type" + workloadFacetKind = "workload_kind" + workloadFacetScheduleType = "schedule_type" + workloadFacetStatus = "status" + workloadFacetOwner = "owner" + workloadFacetGPUResource = "gpu_resource" + + workloadFacetCount = 6 +) + +var workloadModelBoosterListGVK = schema.GroupVersionKind{ + Group: "workload.serving.volcano.sh", + Version: "v1alpha1", + Kind: "ModelBoosterList", +} + +// WorkloadResp is the common row returned by /vcjobs/workloads. Existing job +// fields are deliberately preserved so the job table can render it directly, +// while workloadKind and detailPath make the Kthena rows unambiguous. +type WorkloadResp struct { + WorkloadID string `json:"workloadID"` + WorkloadKind string `json:"workloadKind"` + Scheduler string `json:"scheduler"` + DetailPath string `json:"detailPath"` + StatusDetail string `json:"statusDetail,omitempty"` + Namespace string `json:"namespace,omitempty"` + Model string `json:"model,omitempty"` + + Name string `json:"name"` + JobName string `json:"jobName"` + Owner string `json:"owner"` + UserInfo model.UserInfo `json:"userInfo"` + JobType string `json:"jobType"` + ScheduleType model.ScheduleType `json:"scheduleType"` + WaitingToleranceSeconds *int64 `json:"waitingToleranceSeconds,omitempty"` + Queue string `json:"queue"` + Status string `json:"status"` + CreationTimestamp metav1.Time `json:"createdAt"` + RunningTimestamp metav1.Time `json:"startedAt"` + CompletedTimestamp metav1.Time `json:"completedAt"` + Nodes []string `json:"nodes"` + Resources corev1.ResourceList `json:"resources"` + Locked bool `json:"locked"` + PermanentLocked bool `json:"permanentLocked"` + LockedTimestamp metav1.Time `json:"lockedTimestamp"` + BilledPointsTotal float64 `json:"billedPointsTotal"` +} + +// GetSelfWorkloads godoc +// +// @Summary Get current user's unified workloads +// @Description Lists persisted Volcano jobs and current-user Kthena ModelBoosters as a single pageable list. +// @Tags VolcanoJob +// @Produce json +// @Security Bearer +// @Param page query int false "Page number" +// @Param page_size query int false "Page size, 1-200" +// @Param sort query string false "Sort fields" +// @Param search query string false "Search workloads" +// @Param days query int false "Number of days to look back, -1 for all" +// @Param job_type query []string false "Job types, including model-deployment" collectionFormat(multi) +// @Param workload_kind query []string false "Workload kinds" collectionFormat(multi) +// @Param schedule_type query []int false "Schedule types" collectionFormat(multi) +// @Param status query []string false "Workload statuses" collectionFormat(multi) +// @Param node query string false "Node name" +// @Success 200 {object} resputil.Response[resputil.Page[WorkloadResp]] +// @Router /v1/vcjobs/workloads [get] +func (mgr *VolcanojobMgr) GetSelfWorkloads(c *gin.Context) { + token := util.GetToken(c) + mgr.listWorkloads(c, -1, jobListScope{UserID: &token.UserID, AccountID: &token.AccountID}) +} + +// GetSelfWorkloadFacets godoc +// +// @Summary Get current user's unified workload facets +// @Tags VolcanoJob +// @Produce json +// @Security Bearer +// @Success 200 {object} resputil.Response[resputil.FacetResponse] +// @Router /v1/vcjobs/workloads/facets [get] +func (mgr *VolcanojobMgr) GetSelfWorkloadFacets(c *gin.Context) { + token := util.GetToken(c) + mgr.listWorkloadFacets(c, -1, jobListScope{UserID: &token.UserID, AccountID: &token.AccountID}, false) +} + +// The admin variants match the existing /vcjobs/all semantics. They are also +// useful to administrative tables without changing the legacy APIs. +func (mgr *VolcanojobMgr) GetAllWorkloads(c *gin.Context) { + mgr.listWorkloads(c, allJobsDefaultDays, jobListScope{}) +} + +func (mgr *VolcanojobMgr) GetAllWorkloadFacets(c *gin.Context) { + mgr.listWorkloadFacets(c, allJobsDefaultDays, jobListScope{}, true) +} + +func (mgr *VolcanojobMgr) GetUserWorkloads(c *gin.Context) { + scope, ok := resolveJobUserScope(c) + if !ok { + return + } + mgr.listWorkloads(c, userJobsDefaultDays, scope) +} + +func (mgr *VolcanojobMgr) GetUserWorkloadFacets(c *gin.Context) { + scope, ok := resolveJobUserScope(c) + if !ok { + return + } + mgr.listWorkloadFacets(c, userJobsDefaultDays, scope, false) +} + +func (mgr *VolcanojobMgr) listWorkloads(c *gin.Context, defaultDays int, scope jobListScope) { + request, err := bindJobListQuery(c, true) + if err != nil { + resputil.HandleError(c, err) + return + } + workloads, err := mgr.findWorkloads(c.Request.Context(), scope, &request, defaultDays) + if err != nil { + resputil.HandleError(c, bizerr.Internal.ServiceError.Wrap(err, "list workloads failed")) + return + } + + total := len(workloads) + start := request.offset() + if start > total { + start = total + } + end := start + request.PageSize + if end > total { + end = total + } + resputil.Success(c, resputil.NewPage(workloads[start:end], int64(total), request.Page, request.PageSize)) +} + +func (mgr *VolcanojobMgr) listWorkloadFacets( + c *gin.Context, + defaultDays int, + scope jobListScope, + includeOverview bool, +) { + request, err := bindJobListQuery(c, false) + if err != nil { + resputil.HandleError(c, err) + return + } + facets, err := mgr.findWorkloadFacets(c.Request.Context(), scope, &request, defaultDays, includeOverview) + if err != nil { + resputil.HandleError(c, bizerr.Internal.ServiceError.Wrap(err, "list workload facets failed")) + return + } + resputil.Success(c, resputil.FacetResponse{Facets: facets}) +} + +func (mgr *VolcanojobMgr) findWorkloads( + ctx context.Context, + scope jobListScope, + request *jobListQuery, + defaultDays int, +) ([]WorkloadResp, error) { + workloads := make([]WorkloadResp, 0) + if includesWorkloadKind(request.WorkloadKinds, workloadKindVolcanoJob) && includesVolcanoJobType(request.JobTypes) { + jobs, err := findWorkloadJobs(ctx, scope, request, defaultDays) + if err != nil { + return nil, err + } + for _, job := range jobs { + workloads = append(workloads, workloadFromJob(job)) + } + } + if mgr.isKthenaInferenceEnabled(ctx) && + includesWorkloadKind(request.WorkloadKinds, workloadKindKthenaInference) && + includesKthenaJobType(request.JobTypes) { + inference, err := mgr.findKthenaWorkloads(ctx, scope, request, defaultDays) + if err != nil { + return nil, err + } + workloads = append(workloads, inference...) + } + sortWorkloads(workloads, request.sorts) + return workloads, nil +} + +// isKthenaInferenceEnabled keeps unified workload surfaces consistent with the +// model-deployment feature gate. A missing ConfigService is deliberately +// treated as disabled: Kthena is an opt-in capability for existing installs. +func (mgr *VolcanojobMgr) isKthenaInferenceEnabled(ctx context.Context) bool { + return mgr.configService != nil && mgr.configService.IsKthenaInferenceEnabled(ctx) +} + +func findWorkloadJobs( + ctx context.Context, + scope jobListScope, + request *jobListQuery, + defaultDays int, +) ([]*model.Job, error) { + jobRequest := *request + jobRequest.JobTypes = regularJobTypes(request.JobTypes) + return applyJobFilters(ctx, scope, &jobRequest, defaultDays). + Preload(query.Job.User). + Preload(query.Job.Account). + Find() +} + +func (mgr *VolcanojobMgr) findKthenaWorkloads( + ctx context.Context, + scope jobListScope, + request *jobListQuery, + defaultDays int, +) ([]WorkloadResp, error) { + // ModelBoosters do not expose a useful node until a runtime pod exists. A + // node filter therefore must not return a false-positive inference row. + if request.Node != nil || mgr.client == nil { + return []WorkloadResp{}, nil + } + + labels := client.MatchingLabels{workloadKthenaManagedByLabel: workloadKthenaManagedByValue} + if scope.UserID != nil { + labels[workloadKthenaUserIDLabel] = strconv.FormatUint(uint64(*scope.UserID), 10) + } + if scope.AccountID != nil { + labels[workloadKthenaAccountIDLabel] = strconv.FormatUint(uint64(*scope.AccountID), 10) + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(workloadModelBoosterListGVK) + namespace := mgr.workloadNamespace + if namespace == "" { + namespace = config.GetConfig().Namespaces.Job + } + if err := mgr.client.List(ctx, list, client.InNamespace(namespace), labels); err != nil { + // Kthena is optional for an existing Crater installation. In that case a + // unified list remains useful for normal Volcano jobs. + if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { + return []WorkloadResp{}, nil + } + return nil, err + } + + result := make([]WorkloadResp, 0, len(list.Items)) + for i := range list.Items { + workload := workloadFromModelBooster(&list.Items[i]) + if workloadMatchesQuery(&workload, request, defaultDays) { + result = append(result, workload) + } + } + return result, nil +} + +func workloadFromJob(job *model.Job) WorkloadResp { + response := convertJobResp([]*model.Job{job})[0] + return WorkloadResp{ + WorkloadID: workloadKindVolcanoJob + ":" + response.JobName, + WorkloadKind: workloadKindVolcanoJob, + Scheduler: VolcanoSchedulerName, + DetailPath: "/portal/jobs/detail/" + url.PathEscape(response.JobName), + Name: response.Name, + JobName: response.JobName, + Owner: response.Owner, + UserInfo: response.UserInfo, + JobType: response.JobType, + ScheduleType: response.ScheduleType, + WaitingToleranceSeconds: response.WaitingToleranceSeconds, + Queue: response.Queue, + Status: response.Status, + CreationTimestamp: response.CreationTimestamp, + RunningTimestamp: response.RunningTimestamp, + CompletedTimestamp: response.CompletedTimestamp, + Nodes: response.Nodes, + Resources: response.Resources, + Locked: response.Locked, + PermanentLocked: response.PermanentLocked, + LockedTimestamp: response.LockedTimestamp, + BilledPointsTotal: response.BilledPointsTotal, + } +} + +func workloadFromModelBooster(obj *unstructured.Unstructured) WorkloadResp { + backend, _, _ := unstructured.NestedMap(obj.Object, "spec", "backend") + worker := firstKthenaWorker(backend) + annotations := obj.GetAnnotations() + owner := strings.TrimSpace(annotations[workloadKthenaUserAnnotation]) + queue := strings.TrimSpace(annotations[workloadKthenaAccountAnno]) + phase := kthenaModelBoosterPhase(obj) + scheduler := strings.TrimSpace(stringFromMap(backend, "schedulerName")) + if scheduler == "" { + scheduler = VolcanoSchedulerName + } + modelName := servedModelName(backend, worker) + + return WorkloadResp{ + WorkloadID: workloadKindKthenaInference + ":" + obj.GetName(), + WorkloadKind: workloadKindKthenaInference, + Scheduler: scheduler, + DetailPath: "/portal/inference-services/" + url.PathEscape(obj.GetName()), + StatusDetail: phase, + Namespace: obj.GetNamespace(), + Model: modelName, + Name: obj.GetName(), + JobName: obj.GetName(), + Owner: owner, + UserInfo: model.UserInfo{Username: owner}, + JobType: workloadJobTypeModelDeploy, + ScheduleType: model.ScheduleTypeNormal, + Queue: queue, + Status: kthenaPhaseToJobStatus(phase), + CreationTimestamp: metav1.NewTime(obj.GetCreationTimestamp().Time), + Nodes: []string{}, + Resources: kthenaWorkerResources(backend, worker), + LockedTimestamp: metav1.NewTime(time.Time{}), + BilledPointsTotal: 0, + } +} + +func firstKthenaWorker(backend map[string]any) map[string]any { + workers, _ := backend["workers"].([]any) + if len(workers) == 0 { + return map[string]any{} + } + worker, _ := workers[0].(map[string]any) + if worker == nil { + return map[string]any{} + } + return worker +} + +func servedModelName(backend, worker map[string]any) string { + if configMap, ok := worker["config"].(map[string]any); ok { + if served := strings.TrimSpace(stringFromMap(configMap, "served-model-name")); served != "" { + return served + } + } + return strings.TrimSpace(stringFromMap(backend, "modelURI")) +} + +func kthenaModelBoosterPhase(obj *unstructured.Unstructured) string { + phase, _, _ := unstructured.NestedString(obj.Object, "status", "phase") + if phase = strings.TrimSpace(phase); phase != "" { + return phase + } + conditions, _, _ := unstructured.NestedSlice(obj.Object, "status", "conditions") + for _, raw := range conditions { + condition, _ := raw.(map[string]any) + if strings.EqualFold(stringFromMap(condition, "type"), workloadStatusFailed) && + strings.EqualFold(stringFromMap(condition, "status"), workloadConditionTrue) { + return workloadStatusFailed + } + if strings.EqualFold(stringFromMap(condition, "type"), workloadConditionActive) && + strings.EqualFold(stringFromMap(condition, "status"), workloadConditionTrue) { + return workloadPhaseReady + } + } + return workloadStatusPending +} + +func kthenaPhaseToJobStatus(phase string) string { + switch strings.ToLower(strings.TrimSpace(phase)) { + case "ready", "active", "running": + return workloadStatusRunning + case "completed", "succeeded", "terminated": + return workloadStatusCompleted + case "failed", "error", "degraded": + return workloadStatusFailed + default: + return workloadStatusPending + } +} + +func kthenaWorkerResources(backend, worker map[string]any) corev1.ResourceList { + resourceMap, _ := worker["resources"].(map[string]any) + requests, _ := resourceMap["requests"].(map[string]any) + limits, _ := resourceMap["limits"].(map[string]any) + // Crater writes CPU and memory to requests, but accelerator resources to + // limits (the conventional Kubernetes representation). Merge both maps + // so the unified workload row shows the actual GPU allocation as well. + resourceValues := make(map[string]any, len(requests)+len(limits)) + for name, raw := range limits { + resourceValues[name] = raw + } + for name, raw := range requests { + resourceValues[name] = raw + } + resources := make(corev1.ResourceList, len(resourceValues)) + multiplier := positiveKthenaInt(backend["replicas"]) * + positiveKthenaInt(worker["replicas"]) * positiveKthenaInt(worker["pods"]) + for name, raw := range resourceValues { + quantity, err := resource.ParseQuantity(strings.TrimSpace(valueString(raw))) + if err != nil { + continue + } + quantity.Mul(multiplier) + resources[corev1.ResourceName(name)] = quantity + } + return resources +} + +func positiveKthenaInt(value any) int64 { + switch typed := value.(type) { + case int64: + if typed > 0 { + return typed + } + case int: + if typed > 0 { + return int64(typed) + } + case float64: + if typed > 0 { + return int64(typed) + } + case string: + if parsed, err := strconv.ParseInt(typed, 10, 64); err == nil && parsed > 0 { + return parsed + } + } + return 1 +} + +func stringFromMap(values map[string]any, key string) string { + if values == nil { + return "" + } + return valueString(values[key]) +} + +func valueString(value any) string { + switch typed := value.(type) { + case string: + return typed + case int64: + return strconv.FormatInt(typed, 10) + case int: + return strconv.Itoa(typed) + case float64: + return strconv.FormatFloat(typed, 'f', -1, 64) + default: + return "" + } +} + +func workloadMatchesQuery(workload *WorkloadResp, request *jobListQuery, defaultDays int) bool { + if !includesWorkloadKind(request.WorkloadKinds, workload.WorkloadKind) || + (len(request.JobTypes) > 0 && !containsString(request.JobTypes, workload.JobType)) || + (len(request.ScheduleTypes) > 0 && !containsScheduleType(request.ScheduleTypes, workload.ScheduleType)) || + (len(request.Statuses) > 0 && !containsString(request.Statuses, workload.Status)) { + return false + } + if days := request.days(defaultDays); days != -1 && workload.CreationTimestamp.Time.Before(time.Now().AddDate(0, 0, -days)) { + return false + } + if request.Search == "" { + return true + } + needle := strings.ToLower(request.Search) + searchableValues := []string{ + workload.Name, + workload.JobName, + workload.Owner, + workload.UserInfo.Nickname, + workload.Queue, + workload.Model, + } + for _, value := range searchableValues { + if strings.Contains(strings.ToLower(value), needle) { + return true + } + } + return false +} + +func includesWorkloadKind(kinds []string, kind string) bool { + return len(kinds) == 0 || containsString(kinds, kind) +} + +func includesVolcanoJobType(jobTypes []string) bool { + if len(jobTypes) == 0 { + return true + } + for _, jobType := range jobTypes { + if jobType != workloadJobTypeModelDeploy { + return true + } + } + return false +} + +func includesKthenaJobType(jobTypes []string) bool { + return len(jobTypes) == 0 || containsString(jobTypes, workloadJobTypeModelDeploy) +} + +func regularJobTypes(jobTypes []string) []string { + if len(jobTypes) == 0 { + return nil + } + result := make([]string, 0, len(jobTypes)) + for _, jobType := range jobTypes { + if jobType != workloadJobTypeModelDeploy { + result = append(result, jobType) + } + } + return result +} + +func containsString(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} + +func containsScheduleType(values []int, want model.ScheduleType) bool { + for _, value := range values { + if value == int(want) { + return true + } + } + return false +} + +func sortWorkloads(workloads []WorkloadResp, sorts []jobSort) { + sort.SliceStable(workloads, func(i, j int) bool { + for _, item := range sorts { + comparison := compareWorkloads(&workloads[i], &workloads[j], item.field) + if comparison == 0 { + continue + } + if item.descending { + return comparison > 0 + } + return comparison < 0 + } + return workloads[i].WorkloadID < workloads[j].WorkloadID + }) +} + +func compareWorkloads(left, right *WorkloadResp, field string) int { + switch field { + case workloadSortCreatedAt: + return left.CreationTimestamp.Compare(right.CreationTimestamp.Time) + case workloadSortStartedAt: + return left.RunningTimestamp.Compare(right.RunningTimestamp.Time) + case workloadSortCompletedAt: + return left.CompletedTimestamp.Compare(right.CompletedTimestamp.Time) + case workloadSortScheduleType: + return int(left.ScheduleType) - int(right.ScheduleType) + case workloadSortBilledPointsTotal: + return compareFloat(left.BilledPointsTotal, right.BilledPointsTotal) + case workloadSortName: + return strings.Compare(left.Name, right.Name) + case workloadSortJobName: + return strings.Compare(left.JobName, right.JobName) + case workloadSortOwner: + return strings.Compare(left.Owner, right.Owner) + case workloadSortQueue: + return strings.Compare(left.Queue, right.Queue) + case workloadSortJobType: + return strings.Compare(left.JobType, right.JobType) + case workloadFieldStatus: + return strings.Compare(left.Status, right.Status) + case workloadSortID: + return strings.Compare(left.WorkloadID, right.WorkloadID) + default: + return 0 + } +} + +func compareFloat(left, right float64) int { + if left < right { + return -1 + } + if left > right { + return 1 + } + return 0 +} + +func (mgr *VolcanojobMgr) findWorkloadFacets( + ctx context.Context, + scope jobListScope, + request *jobListQuery, + defaultDays int, + includeOverview bool, +) (map[string][]resputil.FacetItem, error) { + result := make(map[string][]resputil.FacetItem, workloadFacetCount) + for _, facet := range []string{workloadFacetJobType, workloadFacetKind, workloadFacetScheduleType, workloadFacetStatus} { + facetRequest := jobFacetQuery(request, facet) + workloads, err := mgr.findWorkloads(ctx, scope, &facetRequest, defaultDays) + if err != nil { + return nil, err + } + switch facet { + case workloadFacetJobType: + result[facet] = workloadStringFacet(workloads, func(workload *WorkloadResp) string { return workload.JobType }) + case workloadFacetKind: + result[facet] = workloadStringFacet(workloads, func(workload *WorkloadResp) string { return workload.WorkloadKind }) + case workloadFacetScheduleType: + result[facet] = workloadStringFacet(workloads, func(workload *WorkloadResp) string { + return strconv.Itoa(int(workload.ScheduleType)) + }) + case workloadFacetStatus: + result[facet] = workloadStringFacet(workloads, func(workload *WorkloadResp) string { return workload.Status }) + } + } + if !includeOverview { + return result, nil + } + runningRequest := *request + runningRequest.Statuses = []string{workloadStatusRunning} + running, err := mgr.findWorkloads(ctx, scope, &runningRequest, defaultDays) + if err != nil { + return nil, err + } + result[workloadFacetOwner] = workloadStringFacet(running, func(workload *WorkloadResp) string { return workload.Owner }) + result[workloadFacetGPUResource] = workloadGPUFacet(running) + return result, nil +} + +func workloadStringFacet(workloads []WorkloadResp, value func(*WorkloadResp) string) []resputil.FacetItem { + counts := make(map[string]int64) + for i := range workloads { + if item := value(&workloads[i]); item != "" { + counts[item]++ + } + } + return sortedFacetItems(counts) +} + +func workloadGPUFacet(workloads []WorkloadResp) []resputil.FacetItem { + counts := make(map[string]int64) + for i := range workloads { + for name, quantity := range workloads[i].Resources { + if !strings.Contains(strings.ToLower(string(name)), "gpu") { + continue + } + counts[strings.TrimPrefix(string(name), "nvidia.com/")] += quantity.Value() + } + } + return sortedFacetItems(counts) +} + +func sortedFacetItems(counts map[string]int64) []resputil.FacetItem { + items := make([]resputil.FacetItem, 0, len(counts)) + for value, count := range counts { + items = append(items, resputil.FacetItem{Value: value, Count: count}) + } + sort.Slice(items, func(i, j int) bool { + if items[i].Count == items[j].Count { + return items[i].Value < items[j].Value + } + return items[i].Count > items[j].Count + }) + return items +} diff --git a/backend/internal/handler/vcjob/workload_test.go b/backend/internal/handler/vcjob/workload_test.go new file mode 100644 index 000000000..02c3c73ee --- /dev/null +++ b/backend/internal/handler/vcjob/workload_test.go @@ -0,0 +1,181 @@ +package vcjob + +import ( + "context" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + controllerfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/raids-lab/crater/dao/model" +) + +var workloadModelBoosterGVK = schema.GroupVersionKind{ + Group: workloadModelBoosterListGVK.Group, + Version: workloadModelBoosterListGVK.Version, + Kind: "ModelBooster", +} + +func TestWorkloadFromModelBooster(t *testing.T) { + t.Parallel() + createdAt := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC) + booster := testModelBooster("qwen-service", "1", "2", workloadPhaseReady, createdAt) + booster.Object["spec"].(map[string]any)["backend"].(map[string]any)["replicas"] = int64(2) + resources := booster.Object["spec"].(map[string]any)["backend"].(map[string]any)["workers"].([]any)[0].(map[string]any)["resources"].(map[string]any) + requests := resources["requests"].(map[string]any) + limits := resources["limits"].(map[string]any) + delete(requests, "nvidia.com/gpu") + limits["nvidia.com/gpu"] = "1" + + workload := workloadFromModelBooster(booster) + if workload.WorkloadID != "kthena-inference:qwen-service" { + t.Fatalf("workloadID = %q", workload.WorkloadID) + } + if workload.WorkloadKind != workloadKindKthenaInference || workload.Scheduler != VolcanoSchedulerName { + t.Fatalf("kind/scheduler = %q/%q", workload.WorkloadKind, workload.Scheduler) + } + if workload.DetailPath != "/portal/inference-services/qwen-service" { + t.Fatalf("detailPath = %q", workload.DetailPath) + } + if workload.JobType != workloadJobTypeModelDeploy || + workload.Status != workloadStatusRunning || + workload.StatusDetail != workloadPhaseReady { + t.Fatalf("type/status/detail = %q/%q/%q", workload.JobType, workload.Status, workload.StatusDetail) + } + if workload.Model != "Qwen/Qwen3-4B" || workload.Owner != "alice" || workload.Queue != "research" { + t.Fatalf("model/owner/queue = %q/%q/%q", workload.Model, workload.Owner, workload.Queue) + } + if !workload.CreationTimestamp.Time.Equal(createdAt) { + t.Fatalf("createdAt = %v, want %v", workload.CreationTimestamp.Time, createdAt) + } + if gpu, ok := workload.Resources["nvidia.com/gpu"]; !ok || gpu.Value() != 2 { + got := int64(0) + if ok { + got = gpu.Value() + } + t.Fatalf("gpu resource = %d, want 2", got) + } +} + +func TestWorkloadFromModelBoosterRecognizesFailedCondition(t *testing.T) { + t.Parallel() + booster := testModelBooster("failed-service", "1", "2", "", time.Now()) + booster.Object["status"] = map[string]any{ + "conditions": []any{map[string]any{"type": workloadStatusFailed, workloadFieldStatus: workloadConditionTrue}}, + } + + workload := workloadFromModelBooster(booster) + if workload.StatusDetail != workloadStatusFailed || workload.Status != workloadStatusFailed { + t.Fatalf("status/detail = %q/%q, want Failed/Failed", workload.Status, workload.StatusDetail) + } +} + +func TestFindKthenaWorkloadsScopesByUserAndAccountLabels(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + scheme.AddKnownTypeWithName(workloadModelBoosterGVK, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(workloadModelBoosterListGVK, &unstructured.UnstructuredList{}) + manager := &VolcanojobMgr{ + client: controllerfake.NewClientBuilder().WithScheme(scheme).WithObjects( + testModelBooster("owned", "11", "22", "Ready", time.Now()), + testModelBooster("other-account", "11", "23", "Ready", time.Now()), + testModelBooster("other-user", "12", "22", "Ready", time.Now()), + ).Build(), + workloadNamespace: "crater-workspace", + } + userID, accountID := uint(11), uint(22) + workloads, err := manager.findKthenaWorkloads(context.Background(), jobListScope{ + UserID: &userID, AccountID: &accountID, + }, &jobListQuery{}, -1) + if err != nil { + t.Fatal(err) + } + if len(workloads) != 1 || workloads[0].Name != "owned" { + t.Fatalf("scoped workloads = %#v", workloads) + } +} + +func TestFindWorkloadsHidesKthenaWhenFeatureIsDisabled(t *testing.T) { + t.Parallel() + scheme := runtime.NewScheme() + scheme.AddKnownTypeWithName(workloadModelBoosterGVK, &unstructured.Unstructured{}) + scheme.AddKnownTypeWithName(workloadModelBoosterListGVK, &unstructured.UnstructuredList{}) + manager := &VolcanojobMgr{ + client: controllerfake.NewClientBuilder().WithScheme(scheme).WithObjects( + testModelBooster("hidden", "11", "22", "Ready", time.Now()), + ).Build(), + workloadNamespace: "crater-workspace", + } + request := &jobListQuery{ + JobTypes: []string{workloadJobTypeModelDeploy}, + WorkloadKinds: []string{workloadKindKthenaInference}, + } + + workloads, err := manager.findWorkloads(context.Background(), jobListScope{}, request, -1) + if err != nil { + t.Fatal(err) + } + if len(workloads) != 0 { + t.Fatalf("disabled Kthena feature returned workloads: %#v", workloads) + } +} + +func TestUnifiedWorkloadQueryAcceptsModelDeploymentFilters(t *testing.T) { + t.Parallel() + request := &jobListQuery{JobTypes: []string{workloadJobTypeModelDeploy}, WorkloadKinds: []string{workloadKindKthenaInference}} + if err := validateJobListEnums(request); err != nil { + t.Fatal(err) + } + if includesVolcanoJobType(request.JobTypes) || !includesKthenaJobType(request.JobTypes) { + t.Fatal("model-deployment should select only Kthena workloads") + } + if !workloadMatchesQuery(&WorkloadResp{ + WorkloadKind: workloadKindKthenaInference, + JobType: workloadJobTypeModelDeploy, + ScheduleType: model.ScheduleTypeNormal, + Status: workloadStatusRunning, + }, request, -1) { + t.Fatal("Kthena workload did not match its model-deployment filter") + } +} + +func testModelBooster(name, userID, accountID, phase string, createdAt time.Time) *unstructured.Unstructured { + booster := &unstructured.Unstructured{Object: map[string]any{ + "spec": map[string]any{ + "backend": map[string]any{ + "schedulerName": "volcano", + "modelURI": "hf://Qwen/Qwen3-4B", + "replicas": int64(1), + "workers": []any{map[string]any{ + "replicas": int64(1), + "pods": int64(1), + "config": map[string]any{"served-model-name": "Qwen/Qwen3-4B"}, + "resources": map[string]any{"requests": map[string]any{ + "cpu": "4", + "memory": "16Gi", + "nvidia.com/gpu": "1", + }, "limits": map[string]any{}}, + }}, + }, + }, + workloadFieldStatus: map[string]any{"phase": phase}, + }} + booster.SetGroupVersionKind(workloadModelBoosterGVK) + booster.SetName(name) + booster.SetNamespace("crater-workspace") + booster.SetCreationTimestamp(metav1.NewTime(createdAt)) + booster.SetLabels(map[string]string{ + workloadKthenaManagedByLabel: workloadKthenaManagedByValue, + workloadKthenaUserIDLabel: userID, + workloadKthenaAccountIDLabel: accountID, + }) + booster.SetAnnotations(map[string]string{ + workloadKthenaUserAnnotation: "alice", + workloadKthenaAccountAnno: "research", + }) + return booster +} diff --git a/backend/internal/service/config_service.go b/backend/internal/service/config_service.go index ae73d7d86..8db022558 100644 --- a/backend/internal/service/config_service.go +++ b/backend/internal/service/config_service.go @@ -104,6 +104,7 @@ func (s *ConfigService) SetCronJobManager(cjm *cronjob.CronJobManager) { func defaultSystemConfigValue(key string) string { switch key { case model.ConfigKeyEnableGpuAnalysis, + model.ConfigKeyEnableKthenaInference, model.ConfigKeyEnableBillingFeature, model.ConfigKeyEnableBillingActive, model.ConfigKeyEnableRunningSettlement, @@ -433,6 +434,35 @@ func (s *ConfigService) IsGpuAnalysisEnabled(ctx context.Context) bool { return enabled } +// IsKthenaInferenceEnabled reports whether users can access Kthena model +// deployment APIs. Missing, malformed, or unreadable configuration is treated +// as disabled so that the feature remains opt-in. +func (s *ConfigService) IsKthenaInferenceEnabled(ctx context.Context) bool { + configs, err := s.getConfigs(ctx, model.ConfigKeyEnableKthenaInference) + if err != nil { + klog.Errorf("[ConfigService] get Kthena inference feature config failed: %v", err) + return false + } + + enabled, err := strconv.ParseBool(configs[model.ConfigKeyEnableKthenaInference]) + if err != nil { + if value := configs[model.ConfigKeyEnableKthenaInference]; value != "" { + klog.Warningf("[ConfigService] invalid Kthena inference feature config: %q", value) + } + return false + } + return enabled +} + +// SetKthenaInferenceEnabled updates the opt-in switch for Kthena model +// deployment. updateConfigs also creates the key for installations that were +// initialized before this switch was introduced. +func (s *ConfigService) SetKthenaInferenceEnabled(ctx context.Context, enabled bool) error { + return s.updateConfigs(ctx, map[string]string{ + model.ConfigKeyEnableKthenaInference: strconv.FormatBool(enabled), + }) +} + // ResetLLMConfig 重置 LLM 配置并关闭 GPU 分析 func (s *ConfigService) ResetLLMConfig(ctx context.Context) error { return s.q.Transaction(func(tx *query.Query) error { diff --git a/backend/internal/service/config_service_test.go b/backend/internal/service/config_service_test.go index e366bf17b..d47c24a02 100644 --- a/backend/internal/service/config_service_test.go +++ b/backend/internal/service/config_service_test.go @@ -54,6 +54,33 @@ func TestModelDownloadLimitConfigDefaultsAndUpdate(t *testing.T) { } } +func TestKthenaInferenceEnabledDefaultsOffAndUpdates(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file:kthena_inference_config?mode=memory&cache=shared"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.SystemConfig{}, &model.PrequeueConfig{}); err != nil { + t.Fatal(err) + } + service := NewConfigService(query.Use(db)) + + if service.IsKthenaInferenceEnabled(t.Context()) { + t.Fatal("Kthena inference feature should default to disabled") + } + if err := service.SetKthenaInferenceEnabled(t.Context(), true); err != nil { + t.Fatal(err) + } + if !service.IsKthenaInferenceEnabled(t.Context()) { + t.Fatal("Kthena inference feature should be enabled after update") + } + if err := service.SetKthenaInferenceEnabled(t.Context(), false); err != nil { + t.Fatal(err) + } + if service.IsKthenaInferenceEnabled(t.Context()) { + t.Fatal("Kthena inference feature should be disabled after update") + } +} + func TestParsePrequeueRuntimeConfig(t *testing.T) { t.Parallel() diff --git a/charts/crater/templates/crater-backend/serviceaccount.yaml b/charts/crater/templates/crater-backend/serviceaccount.yaml index 1cfb0aa80..9f8c2c794 100644 --- a/charts/crater/templates/crater-backend/serviceaccount.yaml +++ b/charts/crater/templates/crater-backend/serviceaccount.yaml @@ -43,6 +43,12 @@ rules: - apiGroups: ["scheduling.volcano.sh"] resources: ["queues"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["workload.serving.volcano.sh"] + resources: ["modelboosters", "modelservings", "autoscalingpolicies"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["networking.serving.volcano.sh"] + resources: ["modelroutes", "modelservers"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch", "update", "patch"] @@ -52,6 +58,9 @@ rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: [""] + resources: ["services/proxy"] + verbs: ["get", "create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -112,4 +121,4 @@ subjects: roleRef: kind: Role name: crater-leader-election - apiGroup: rbac.authorization.k8s.io \ No newline at end of file + apiGroup: rbac.authorization.k8s.io diff --git a/frontend/src/components/badge/job-type-badge.tsx b/frontend/src/components/badge/job-type-badge.tsx index bfac74d1e..22b150c00 100644 --- a/frontend/src/components/badge/job-type-badge.tsx +++ b/frontend/src/components/badge/job-type-badge.tsx @@ -50,6 +50,10 @@ export const jobTypes = [ value: 'openmpi', label: 'OpenMPI', }, + { + value: 'model-deployment', + label: '模型部署', + }, ] const getJobTypeLabel = ( @@ -108,6 +112,12 @@ const getJobTypeLabel = ( color: 'text-highlight-green bg-highlight-green/10', description: 'OpenMPI 作业', } + case JobType.ModelDeployment: + return { + label: '模型部署', + color: 'text-highlight-blue bg-highlight-blue/10', + description: 'Kthena 在线模型部署', + } default: return { label: 'Custom', diff --git a/frontend/src/components/badge/kthena-status-badge.tsx b/frontend/src/components/badge/kthena-status-badge.tsx new file mode 100644 index 000000000..35729ef43 --- /dev/null +++ b/frontend/src/components/badge/kthena-status-badge.tsx @@ -0,0 +1,94 @@ +import { t } from 'i18next' + +import { KthenaService } from '@/services/api/inference' + +import { PhaseBadge, PhaseBadgeData } from './phase-badge' + +export type KthenaDisplayState = + | 'submitted' + | 'scheduling' + | 'deploying' + | 'running' + | 'degraded' + | 'failed' + +const hasProblemDiagnostics = (service: KthenaService) => + service.diagnostics?.some((diagnostic) => { + const reason = diagnostic.reason?.toLowerCase() ?? '' + const message = diagnostic.message?.toLowerCase() ?? '' + return ( + diagnostic.level === 'error' || + reason.includes('backoff') || + reason.includes('failed') || + reason.includes('error') || + reason.includes('unhealthy') || + message.includes('back-off') || + message.includes('readiness probe failed') || + message.includes('exit code') + ) + }) ?? false + +const hasErrorDiagnostics = (service: KthenaService) => + service.diagnostics?.some((diagnostic) => diagnostic.level === 'error') ?? false + +export const getKthenaDisplayState = (service: KthenaService): KthenaDisplayState => { + const phase = service.phase || 'Pending' + if (phase === 'Failed') return 'failed' + if (phase === 'Degraded') return 'degraded' + // Kubernetes Warning events are historical and can remain attached to a pod + // after it becomes ready. Keep them in diagnostics, but don't let a + // transient readiness failure override Kthena's current Ready/Active phase. + // A live error diagnostic still takes precedence so real runtime failures + // remain visible in the header. + if (phase === 'Ready' || phase === 'Active') { + return hasErrorDiagnostics(service) ? 'degraded' : 'running' + } + if (hasProblemDiagnostics(service)) return 'degraded' + if (phase === 'Pending') return service.resources?.length ? 'scheduling' : 'submitted' + return 'deploying' +} + +export const getKthenaStatusLabel = (state: KthenaDisplayState): PhaseBadgeData => { + switch (state) { + case 'submitted': + return { + label: t('kthena.state.submitted'), + color: 'bg-highlight-slate/20 text-highlight-slate', + description: t('kthena.stateDescription.submitted'), + } + case 'scheduling': + return { + label: t('kthena.state.scheduling'), + color: 'bg-highlight-purple/20 text-highlight-purple', + description: t('kthena.stateDescription.scheduling'), + } + case 'deploying': + return { + label: t('kthena.state.deploying'), + color: 'bg-highlight-blue/20 text-highlight-blue', + description: t('kthena.stateDescription.deploying'), + } + case 'running': + return { + label: t('kthena.state.running'), + color: 'bg-highlight-blue/20 text-highlight-blue', + description: t('kthena.stateDescription.running'), + } + case 'degraded': + return { + label: t('kthena.state.degraded'), + color: 'bg-highlight-orange/20 text-highlight-orange', + description: t('kthena.stateDescription.degraded'), + } + case 'failed': + return { + label: t('kthena.state.failed'), + color: 'bg-highlight-red/20 text-highlight-red', + description: t('kthena.stateDescription.failed'), + } + } +} + +export default function KthenaStatusBadge({ service }: { service: KthenaService }) { + return +} diff --git a/frontend/src/components/form/env-form-field.tsx b/frontend/src/components/form/env-form-field.tsx index 96964b1ea..ce268fd86 100644 --- a/frontend/src/components/form/env-form-field.tsx +++ b/frontend/src/components/form/env-form-field.tsx @@ -78,6 +78,7 @@ export function EnvFormCard< render={({ field }) => ( + ) +} const VolcanoOverview = () => { const { t } = useTranslation() @@ -73,14 +115,14 @@ const VolcanoOverview = () => { }) const batchQuery = useQuery({ - queryKey: buildRemoteQueryKey('jobs-batch', tableState.params), - queryFn: async ({ signal }) => (await apiJobBatchList(tableState.params, signal)).data, + queryKey: buildRemoteQueryKey('workloads-batch', tableState.params), + queryFn: async ({ signal }) => (await apiWorkloadBatchList(tableState.params, signal)).data, placeholderData: keepPreviousData, refetchInterval: REFETCH_INTERVAL, }) const facetsQuery = useQuery({ - queryKey: buildFacetQueryKey('jobs-batch', tableState.params), - queryFn: async ({ signal }) => (await apiJobBatchFacets(tableState.params, signal)).data, + queryKey: buildFacetQueryKey('workloads-batch', tableState.params), + queryFn: async ({ signal }) => (await apiWorkloadBatchFacets(tableState.params, signal)).data, }) const toolbarConfig = useMemo( () => getRemoteJobToolbarConfig(facetsQuery.data, batchJobTypes), @@ -91,8 +133,8 @@ const VolcanoOverview = () => { try { // 并行发送所有异步请求 await Promise.all([ - queryClient.invalidateQueries({ queryKey: ['remote-list', 'jobs-batch'] }), - queryClient.invalidateQueries({ queryKey: ['remote-list-facets', 'jobs-batch'] }), + queryClient.invalidateQueries({ queryKey: ['remote-list', 'workloads-batch'] }), + queryClient.invalidateQueries({ queryKey: ['remote-list-facets', 'workloads-batch'] }), queryClient.invalidateQueries({ queryKey: ['job'] }), queryClient.invalidateQueries({ queryKey: ['job', 'billing'] }), queryClient.invalidateQueries({ queryKey: ['aitask', 'quota'] }), @@ -135,7 +177,7 @@ const VolcanoOverview = () => { { accessorKey: 'name', header: ({ column }) => , - cell: ({ row }) => , + cell: ({ row }) => , }, { accessorFn: (row) => getDisplayJobPhase(row.status), @@ -177,7 +219,12 @@ const VolcanoOverview = () => { { accessorKey: 'billedPointsTotal', header: ({ column }) => , - cell: ({ row }) => , + cell: ({ row }) => + row.original.workloadKind === WorkloadKind.KthenaInference ? ( + - + ) : ( + + ), } as ColumnDef, ] : []), @@ -215,8 +262,7 @@ const VolcanoOverview = () => { id: 'actions', enableHiding: false, cell: ({ row }) => { - const jobInfo = row.original - return + return }, }, ], @@ -226,14 +272,15 @@ const VolcanoOverview = () => { return ( row.jobName} + getRowId={(row) => row.workloadID} toolbarConfig={toolbarConfig} + canSelectRow={(row) => row.workloadKind !== WorkloadKind.KthenaInference} briefChildren={} multipleHandlers={[ { diff --git a/frontend/src/components/query-table/remote.tsx b/frontend/src/components/query-table/remote.tsx index 695d05e7d..53be4c779 100644 --- a/frontend/src/components/query-table/remote.tsx +++ b/frontend/src/components/query-table/remote.tsx @@ -26,6 +26,7 @@ interface RemoteDataTableProps extends HTMLAttributes string toolbarConfig?: DataTableToolbarConfig multipleHandlers?: MultipleHandler[] + canSelectRow?: (row: TData) => boolean briefChildren?: React.ReactNode withI18n?: boolean initialColumnVisibility?: VisibilityState @@ -39,6 +40,7 @@ export function RemoteDataTable({ getRowId, toolbarConfig, multipleHandlers, + canSelectRow, children, briefChildren, withI18n = false, @@ -70,6 +72,7 @@ export function RemoteDataTable({ row.toggleSelected(Boolean(value))} + disabled={!row.getCanSelect()} /> ), enableSorting: false, @@ -97,7 +100,9 @@ export function RemoteDataTable({ columnVisibility, rowSelection, }, - enableRowSelection: Boolean(multipleHandlers?.length), + enableRowSelection: multipleHandlers?.length + ? (row) => canSelectRow?.(row.original) ?? true + : false, enableMultiSort: true, maxMultiSortColCount: 3, autoResetPageIndex: false, diff --git a/frontend/src/i18n/locales/enUS/translation.json b/frontend/src/i18n/locales/enUS/translation.json index a1ea019d2..835c49c24 100644 --- a/frontend/src/i18n/locales/enUS/translation.json +++ b/frontend/src/i18n/locales/enUS/translation.json @@ -687,6 +687,200 @@ "jupyter.snapshot.save": "Save", "jupyter.snapshot.success": "Snapshot submitted", "jupyter.snapshot.title": "Save Image", + "kthena.actions.back": "Back", + "kthena.actions.backToList": "Back to list", + "kthena.actions.cancel": "Cancel", + "kthena.actions.clearChat": "Clear chat", + "kthena.actions.clone": "Clone deployment", + "kthena.actions.create": "New deployment", + "kthena.actions.delete": "Delete", + "kthena.actions.deleteDeployment": "Delete deployment", + "kthena.actions.more": "More actions", + "kthena.actions.requesting": "Requesting", + "kthena.actions.send": "Send request", + "kthena.actions.view": "View details", + "kthena.chat.collapseSessions": "Collapse chat list", + "kthena.chat.deleteSession": "Delete chat", + "kthena.chat.emptyTitle": "Start a new conversation", + "kthena.chat.expandSessions": "Expand chat list", + "kthena.chat.newSession": "New chat", + "kthena.chat.sessions": "Chat list", + "kthena.chat.untitledSession": "Untitled chat", + "kthena.copy.generic": "{{label}} copied", + "kthena.copy.image": "Image address copied", + "kthena.copy.model": "Model name copied", + "kthena.copy.resource": "Resource name copied", + "kthena.delete.description": "This will delete Kthena ModelBooster {{name}} and its related workloads.", + "kthena.delete.success": "Model deployment deleted", + "kthena.delete.title": "Delete model deployment", + "kthena.detail.apiBase": "API endpoint", + "kthena.detail.curlContext": "Current chat context", + "kthena.detail.defaultPrompt": "Describe in one sentence how Crater and Kthena work together to serve models.", + "kthena.detail.description": "Kthena ModelBooster online inference service", + "kthena.detail.diagnosticsTitle": "Diagnostics", + "kthena.detail.invokeHint": "CLI calls need a Crater login token; the web test uses the current login session automatically.", + "kthena.detail.invokeInfo": "Invocation info", + "kthena.detail.logDetails": "Container log details", + "kthena.detail.modelName": "Model name", + "kthena.detail.noDiagnostics": "No blocking Pod, event, or container error was found.", + "kthena.detail.noResources": "No related resources found", + "kthena.detail.onlineTest": "Online test", + "kthena.detail.overviewTitle": "Service overview", + "kthena.detail.primaryIssue": "Primary issue", + "kthena.detail.promptPlaceholder": "Send a message to this model deployment", + "kthena.detail.rawResponse": "Raw response", + "kthena.detail.responsePlaceholder": "The response will appear here", + "kthena.detail.restarts": "{{count}} restarts", + "kthena.detail.routeModelName": "Route model name", + "kthena.detail.routeResource": "Route resource", + "kthena.detail.runtimeNode": "Runtime node", + "kthena.detail.runtimePod": "Runtime Pod", + "kthena.detail.runtimePods": "Runtime Pods", + "kthena.detail.runtimeResources": "Runtime resources", + "kthena.detail.servedModel": "Served model", + "kthena.detail.tabs.diagnostics": "Diagnostics", + "kthena.detail.tabs.invoke": "Invoke", + "kthena.detail.tabs.overview": "Overview", + "kthena.detail.tabs.resources": "Kthena resources", + "kthena.detail.tabs.usage": "Resource usage", + "kthena.detail.title": "Model deployment details", + "kthena.detail.usageDescription": "Shows CPU, memory, and accelerator metrics for the selected runtime Pod over the last 15 minutes.", + "kthena.detail.usageNoGrafana": "This environment has no Grafana Pod monitor configured, so live resource usage is unavailable.", + "kthena.detail.usageNoPod": "There is no runtime Pod to monitor yet. Metrics will appear here when the deployment is running.", + "kthena.detail.usageNotScheduled": "The runtime Pod has not been scheduled to a node. Live resource usage will be available after scheduling.", + "kthena.detail.usageTitle": "Resource usage", + "kthena.detail.usageUnavailable": "Resource usage unavailable", + "kthena.detail.waitingModelRoute": "Waiting for ModelRoute", + "kthena.empty": "No model deployments", + "kthena.filter.all": "All", + "kthena.filter.attention": "Attention", + "kthena.filter.progressing": "In progress", + "kthena.filter.ready": "Ready", + "kthena.filter.running": "Running", + "kthena.form.actions.addConfig": "Add parameter", + "kthena.form.cloneDescription": "Create a new deployment by copying {{name}}", + "kthena.form.createSuccess": "Model deployment {{name}} created", + "kthena.form.description": "Prefer platform models; switch to manual URI only for external repositories", + "kthena.form.descriptions.cacheURI": "Where Kthena stores downloaded external models. Default: hostpath:///tmp/cache. Use a dedicated disk or PVC in production.", + "kthena.form.descriptions.gpuModel": "The GPU model is written into Kthena worker limits as a Kubernetes resource name. After selecting a V100/A100/domestic GPU resource name, the scheduler only selects nodes with that resource. If no specific GPU model is selected, node placement follows the platform resource definition.", + "kthena.form.descriptions.imageSource": "Prefer platform images. Switching runtime fills a recommended image address; you can switch back to a platform image later.", + "kthena.form.descriptions.modelURI": "Supports hf://, s3://, pvc://, and ms://", + "kthena.form.descriptions.node": "Use this for debugging, load testing, or avoiding resource fragmentation on a specific machine. By default, the scheduler selects a node.", + "kthena.form.descriptions.pinNode": "When enabled, Kthena inference Pods are scheduled only to the selected node.", + "kthena.form.descriptions.platformModel": "Models currently accessible to this account are listed here", + "kthena.form.descriptions.selectedModel": "Will use {{url}}", + "kthena.form.descriptions.servedModel": "The model name in OpenAI requests. Platform models default to the model resource name.", + "kthena.form.descriptions.serviceName": "Used to generate the Kthena ModelBooster name", + "kthena.form.descriptions.source": "Platform models are converted to a Kthena-compatible pvc:// path. External URIs support hf://, s3://, ms://, and pvc://.", + "kthena.form.descriptions.workerConfig": "worker.config is a Kthena field and becomes inference engine startup parameters. Environment variables are injected into the container environment. They serve different purposes.", + "kthena.form.descriptions.workerImage": "Container image that starts the inference runtime. It must match the selected backend.", + "kthena.form.fields.backend": "Inference backend", + "kthena.form.fields.cacheURI": "Model cache URI", + "kthena.form.fields.configKey": "Parameter name", + "kthena.form.fields.configValue": "Parameter value", + "kthena.form.fields.imageSource": "Image source", + "kthena.form.fields.modelURI": "Model URI", + "kthena.form.fields.node": "Node", + "kthena.form.fields.pinNode": "Pin runtime node", + "kthena.form.fields.platformModel": "Platform model", + "kthena.form.fields.replicas": "Replicas", + "kthena.form.fields.servedModel": "Served model name", + "kthena.form.fields.serviceName": "Service name", + "kthena.form.fields.source": "Source", + "kthena.form.fields.workerImage": "Worker image", + "kthena.form.guide.advanced": "Advanced parameters: written to Kthena worker.config and eventually used as vLLM/SGLang engine startup parameters.", + "kthena.form.guide.cache": "Model cache URI: where external models are cached after download. Platform models usually do not need changes here.", + "kthena.form.guide.env": "Environment variables: injected into the container environment, commonly used for HF_ENDPOINT, ENDPOINT, NCCL_IB_DISABLE, and similar settings.", + "kthena.form.guide.model": "Model service: platform models use model paths registered in Crater. Only external models need a manual URI.", + "kthena.form.guide.resources": "Runtime resources: CPU, memory, and GPU follow job submission conventions. Select a GPU model when GPU count is greater than 0.", + "kthena.form.imageSource.manual": "Manual image address", + "kthena.form.imageSource.platform": "Platform image", + "kthena.form.preset.cpu.description": "Apply CPU resources, vLLM backend, and basic parameters. The model can still be changed to a platform model.", + "kthena.form.preset.cpu.title": "CPU small model", + "kthena.form.preset.gpu.description": "Apply GPU resources, vLLM image, and common throughput parameters", + "kthena.form.preset.sglang.description": "Apply SGLang backend, SGLang image, and context parameters", + "kthena.form.preset.sglang.title": "SGLang service", + "kthena.form.runtime.default": "Controls which inference runtime Kthena starts.", + "kthena.form.runtime.disaggregated": "vLLM prefill/decode disaggregation mode. Requires matching advanced Kthena configuration.", + "kthena.form.runtime.mindie": "Ascend inference runtime. Requires matching MindIE/Ascend image and NPU resources.", + "kthena.form.runtime.sglang": "Runtime for low-latency and complex serving scenarios. Requires an SGLang image.", + "kthena.form.runtime.vllm": "General GPU inference runtime suitable for most OpenAI-compatible model services.", + "kthena.form.sections.advanced": "Advanced parameters", + "kthena.form.sections.env": "Environment variables", + "kthena.form.sections.guide": "Field guide", + "kthena.form.sections.modelSource": "Model source", + "kthena.form.sections.presets": "Runtime presets", + "kthena.form.sections.resources": "Runtime resources", + "kthena.form.sections.runtime": "Inference service", + "kthena.form.sections.scheduling": "Scheduling constraints", + "kthena.form.sections.summary": "Deployment summary", + "kthena.form.selectNode": "Select runtime node", + "kthena.form.selectPlatformModel": "Select platform model", + "kthena.form.source.external": "Manual URI", + "kthena.form.source.platform": "Platform model", + "kthena.form.submit": "Start deployment", + "kthena.form.summary.applied": "Applied", + "kthena.form.summary.custom": "Custom", + "kthena.form.template.delete": "Delete template", + "kthena.form.template.deleteDescription": "Delete “{{name}}”? Existing model deployments will not be affected.", + "kthena.form.template.deleteTitle": "Delete runtime template", + "kthena.form.template.deleted": "Runtime template deleted", + "kthena.form.template.description": "Template description", + "kthena.form.template.dialogDescription": "Save model, image, resources, environment variables, and advanced parameters. It is visible only to you in the current account.", + "kthena.form.template.mine": "My templates", + "kthena.form.template.name": "Template name", + "kthena.form.template.nameRequired": "Enter a template name", + "kthena.form.template.noDescription": "Saved runtime configuration", + "kthena.form.template.save": "Save as template", + "kthena.form.template.saveTitle": "Save runtime template", + "kthena.form.template.saved": "Runtime template saved", + "kthena.form.template.update": "Update template", + "kthena.form.template.updateTitle": "Update runtime template", + "kthena.form.title": "New model deployment", + "kthena.form.validation.configKeyRequired": "Parameter name is required", + "kthena.form.validation.imageRequired": "Image address is required", + "kthena.form.validation.modelURIRequired": "Model URI is required", + "kthena.form.validation.namePattern": "Only lowercase letters, digits, and hyphens are allowed", + "kthena.form.validation.platformImageRequired": "Select a platform image", + "kthena.form.validation.platformModelRequired": "Select a platform model", + "kthena.list.description": "Deploy online inference services through Kthena ModelBooster", + "kthena.list.searchPlaceholder": "Search deployment name", + "kthena.list.showing": "Showing {{count}} deployments", + "kthena.list.syncing": "Syncing Kthena status", + "kthena.loading": "Loading", + "kthena.phase.failed": "Failed", + "kthena.phase.pending": "Pending", + "kthena.phase.progressing": "Deploying", + "kthena.phase.ready": "Ready", + "kthena.phase.unknown": "Unknown", + "kthena.state.degraded": "Degraded", + "kthena.state.deploying": "Deploying", + "kthena.state.failed": "Failed", + "kthena.state.running": "Running", + "kthena.state.scheduling": "Scheduling", + "kthena.state.submitted": "Submitted", + "kthena.stateDescription.degraded": "The deployment has BackOff, health check, or container issues. Check diagnostics.", + "kthena.stateDescription.deploying": "Kthena is creating downstream resources and starting the inference service.", + "kthena.stateDescription.failed": "The deployment failed. Check diagnostics and adjust the configuration.", + "kthena.stateDescription.running": "The inference service is ready to receive requests.", + "kthena.stateDescription.scheduling": "Resources are created and waiting for scheduling or Pod startup.", + "kthena.stateDescription.submitted": "The deployment request was submitted and is waiting for Kthena status sync.", + "kthena.summary.attention": "Attention", + "kthena.summary.backends": "Backend types", + "kthena.summary.progressing": "Deploying", + "kthena.summary.ready": "Ready", + "kthena.summary.running": "Running", + "kthena.summary.total": "Deployments", + "kthena.table.actions": "Actions", + "kthena.table.backend": "Backend", + "kthena.table.createdAt": "Created", + "kthena.table.image": "Image", + "kthena.table.model": "Model", + "kthena.table.name": "Name", + "kthena.table.owner": "User", + "kthena.table.replicas": "Replicas", + "kthena.table.status": "Status", + "kthena.title": "Model Deployments", "loadingIndicator.loadingText": "Loading...", "loginHeatmap.title": "User Activity", "modelDownload.action.delete": "Delete", @@ -864,6 +1058,7 @@ "navigation.jobTemplates": "Job Templates", "navigation.jupyterLab": "Jupyter Lab", "navigation.memberManagement": "Member", + "navigation.modelDeployments": "Model Deployments", "navigation.models": "Models", "navigation.more": "More", "navigation.myImages": "My Images", @@ -1290,6 +1485,14 @@ "systemConfig.gpuAnalysis.switchLabel": "Enable Intelligent Analysis", "systemConfig.gpuAnalysis.title": "GPU Usage Anomaly Analysis", "systemConfig.gpuAnalysis.verifyingLLM": "Verifying LLM service connectivity...", + "systemConfig.kthenaInference.activeNotice": "Model deployment is enabled; users can create online services with Kthena.", + "systemConfig.kthenaInference.description": "Control Kthena-based online model deployment, management, and invocation.", + "systemConfig.kthenaInference.disabledSuccess": "Model deployment has been disabled", + "systemConfig.kthenaInference.disabledWarning": "When disabled, the entry is hidden and direct model-deployment pages and APIs are rejected.", + "systemConfig.kthenaInference.enabledSuccess": "Model deployment has been enabled", + "systemConfig.kthenaInference.switchDescription": "When enabled, users can create, manage, and invoke online model services.", + "systemConfig.kthenaInference.switchLabel": "Enable model deployment", + "systemConfig.kthenaInference.title": "Model deployment", "systemConfig.llm.apiKey": "API Key", "systemConfig.llm.baseUrl": "API Base URL", "systemConfig.llm.description": "Configure the language model service used to power the platform's intelligent analysis features.", diff --git a/frontend/src/i18n/locales/ja/translation.json b/frontend/src/i18n/locales/ja/translation.json index 28c0ffbc1..f81954769 100644 --- a/frontend/src/i18n/locales/ja/translation.json +++ b/frontend/src/i18n/locales/ja/translation.json @@ -687,6 +687,200 @@ "jupyter.snapshot.save": "保存", "jupyter.snapshot.success": "スナップショットが送信されました", "jupyter.snapshot.title": "イメージを保存", + "kthena.actions.back": "戻る", + "kthena.actions.backToList": "一覧に戻る", + "kthena.actions.cancel": "キャンセル", + "kthena.actions.clearChat": "チャットをクリア", + "kthena.actions.clone": "デプロイを複製", + "kthena.actions.create": "新規デプロイ", + "kthena.actions.delete": "削除", + "kthena.actions.deleteDeployment": "デプロイを削除", + "kthena.actions.more": "その他の操作", + "kthena.actions.requesting": "リクエスト中", + "kthena.actions.send": "リクエスト送信", + "kthena.actions.view": "詳細を見る", + "kthena.chat.collapseSessions": "チャット一覧を閉じる", + "kthena.chat.deleteSession": "チャットを削除", + "kthena.chat.emptyTitle": "新しい会話を始める", + "kthena.chat.expandSessions": "チャット一覧を開く", + "kthena.chat.newSession": "新しいチャット", + "kthena.chat.sessions": "チャット一覧", + "kthena.chat.untitledSession": "名称未設定のチャット", + "kthena.copy.generic": "{{label}} をコピーしました", + "kthena.copy.image": "イメージアドレスをコピーしました", + "kthena.copy.model": "モデル名をコピーしました", + "kthena.copy.resource": "リソース名をコピーしました", + "kthena.delete.description": "Kthena ModelBooster {{name}} と関連ワークロードを削除します。", + "kthena.delete.success": "モデルデプロイを削除しました", + "kthena.delete.title": "モデルデプロイを削除", + "kthena.detail.apiBase": "API エンドポイント", + "kthena.detail.curlContext": "現在の会話コンテキスト", + "kthena.detail.defaultPrompt": "Crater と Kthena がどのように連携してモデルサービスを提供するかを一文で説明してください。", + "kthena.detail.description": "Kthena ModelBooster オンライン推論サービス", + "kthena.detail.diagnosticsTitle": "診断", + "kthena.detail.invokeHint": "CLI 呼び出しには Crater ログイントークンが必要です。Web テストでは現在のログイン状態が自動的に使用されます。", + "kthena.detail.invokeInfo": "呼び出し情報", + "kthena.detail.logDetails": "コンテナログ詳細", + "kthena.detail.modelName": "モデル名", + "kthena.detail.noDiagnostics": "デプロイをブロックしている Pod、イベント、コンテナエラーは見つかりません。", + "kthena.detail.noResources": "関連リソースが見つかりません", + "kthena.detail.onlineTest": "オンラインテスト", + "kthena.detail.overviewTitle": "サービス概要", + "kthena.detail.primaryIssue": "主な問題", + "kthena.detail.promptPlaceholder": "このモデルデプロイにメッセージを送信", + "kthena.detail.rawResponse": "生レスポンス", + "kthena.detail.responsePlaceholder": "レスポンスはここに表示されます", + "kthena.detail.restarts": "{{count}} 回再起動", + "kthena.detail.routeModelName": "ルートモデル名", + "kthena.detail.routeResource": "ルートリソース", + "kthena.detail.runtimeNode": "実行ノード", + "kthena.detail.runtimePod": "実行 Pod", + "kthena.detail.runtimePods": "実行 Pod", + "kthena.detail.runtimeResources": "実行リソース", + "kthena.detail.servedModel": "提供モデル", + "kthena.detail.tabs.diagnostics": "診断", + "kthena.detail.tabs.invoke": "呼び出し", + "kthena.detail.tabs.overview": "概要", + "kthena.detail.tabs.resources": "Kthena リソース", + "kthena.detail.tabs.usage": "リソース使用量", + "kthena.detail.title": "モデルデプロイ詳細", + "kthena.detail.usageDescription": "選択した実行 Pod の直近 15 分間の CPU、メモリ、アクセラレータのリアルタイム指標を表示します。", + "kthena.detail.usageNoGrafana": "この環境では Grafana Pod 監視が設定されていないため、リアルタイムのリソース使用量を表示できません。", + "kthena.detail.usageNoPod": "監視できる実行 Pod はまだありません。デプロイが実行状態になると、ここに指標が表示されます。", + "kthena.detail.usageNotScheduled": "実行 Pod はまだノードにスケジュールされていません。スケジュール完了後にリソース使用量を確認できます。", + "kthena.detail.usageTitle": "リソース使用量", + "kthena.detail.usageUnavailable": "リソース使用量を利用できません", + "kthena.detail.waitingModelRoute": "ModelRoute を待機中", + "kthena.empty": "モデルデプロイはありません", + "kthena.filter.all": "すべて", + "kthena.filter.attention": "異常", + "kthena.filter.progressing": "進行中", + "kthena.filter.ready": "利用可能", + "kthena.filter.running": "実行中", + "kthena.form.actions.addConfig": "パラメータを追加", + "kthena.form.cloneDescription": "{{name}} の設定をコピーして新しいデプロイを作成します", + "kthena.form.createSuccess": "モデルデプロイ {{name}} を作成しました", + "kthena.form.description": "まずプラットフォームモデルを使用し、外部リポジトリが必要な場合のみ手動 URI に切り替えます", + "kthena.form.descriptions.cacheURI": "Kthena が外部モデルをダウンロード後に保存する場所です。既定値は hostpath:///tmp/cache です。本番環境では専用ディスクまたは PVC を推奨します。", + "kthena.form.descriptions.gpuModel": "GPU モデルは Kubernetes リソース名として Kthena worker limits に書き込まれます。V100/A100/国産 GPU のリソース名を選択すると、そのリソースを持つノードのみが選ばれます。未指定の場合はプラットフォームのリソース定義に従います。", + "kthena.form.descriptions.imageSource": "プラットフォームイメージを優先してください。推論バックエンドを切り替えると推奨イメージが入力されますが、後でプラットフォームイメージに戻せます。", + "kthena.form.descriptions.modelURI": "hf://、s3://、pvc://、ms:// をサポートします", + "kthena.form.descriptions.node": "特定マシンでのデバッグ、負荷テスト、リソース断片化の回避に使用します。既定ではスケジューラがノードを選択します。", + "kthena.form.descriptions.pinNode": "有効にすると、Kthena が生成する推論 Pod は選択したノードにのみスケジュールされます。", + "kthena.form.descriptions.platformModel": "現在のアカウントでアクセス可能なモデルリソースを表示します", + "kthena.form.descriptions.selectedModel": "{{url}} を使用します", + "kthena.form.descriptions.servedModel": "OpenAI リクエスト内の model 名です。プラットフォームモデルではモデルリソース名が既定です。", + "kthena.form.descriptions.serviceName": "Kthena ModelBooster 名の生成に使用されます", + "kthena.form.descriptions.source": "プラットフォームモデルは Kthena が対応する pvc:// パスに変換されます。外部 URI は hf://、s3://、ms://、pvc:// をサポートします。", + "kthena.form.descriptions.workerConfig": "worker.config は Kthena の定義フィールドで、推論エンジンの起動パラメータに変換されます。環境変数はコンテナ環境に注入されます。用途が異なります。", + "kthena.form.descriptions.workerImage": "実際に推論ランタイムを起動するコンテナイメージです。推論バックエンドと一致する必要があります。", + "kthena.form.fields.backend": "推論バックエンド", + "kthena.form.fields.cacheURI": "モデルキャッシュ URI", + "kthena.form.fields.configKey": "パラメータ名", + "kthena.form.fields.configValue": "パラメータ値", + "kthena.form.fields.imageSource": "イメージソース", + "kthena.form.fields.modelURI": "モデル URI", + "kthena.form.fields.node": "ノード", + "kthena.form.fields.pinNode": "実行ノードを指定", + "kthena.form.fields.platformModel": "プラットフォームモデル", + "kthena.form.fields.replicas": "レプリカ数", + "kthena.form.fields.servedModel": "公開モデル名", + "kthena.form.fields.serviceName": "サービス名", + "kthena.form.fields.source": "ソース", + "kthena.form.fields.workerImage": "Worker イメージ", + "kthena.form.guide.advanced": "高度なパラメータ: Kthena worker.config に書き込まれ、最終的に vLLM/SGLang などの推論エンジン起動パラメータになります。", + "kthena.form.guide.cache": "モデルキャッシュ URI: 外部モデルのダウンロード後キャッシュ場所です。プラットフォームモデルでは通常変更不要です。", + "kthena.form.guide.env": "環境変数: コンテナ環境に注入され、HF_ENDPOINT、ENDPOINT、NCCL_IB_DISABLE などによく使われます。", + "kthena.form.guide.model": "モデルサービス: プラットフォームモデルでは Crater に登録されたモデルパスを使用します。外部モデルのみ手動 URI が必要です。", + "kthena.form.guide.resources": "実行リソース: CPU/メモリ/GPU はジョブ送信と同じです。GPU 数が 0 より大きい場合は GPU モデルを選択してください。", + "kthena.form.imageSource.manual": "手動イメージアドレス", + "kthena.form.imageSource.platform": "プラットフォームイメージ", + "kthena.form.preset.cpu.description": "CPU リソース、vLLM バックエンド、基本パラメータを適用します。モデルは後でプラットフォームモデルに変更できます。", + "kthena.form.preset.cpu.title": "CPU 小型モデル", + "kthena.form.preset.gpu.description": "GPU リソース、vLLM イメージ、一般的なスループットパラメータを適用します", + "kthena.form.preset.sglang.description": "SGLang バックエンド、SGLang イメージ、コンテキストパラメータを適用します", + "kthena.form.preset.sglang.title": "SGLang サービス", + "kthena.form.runtime.default": "Kthena が起動する推論ランタイム種別を決定します。", + "kthena.form.runtime.disaggregated": "vLLM prefill/decode 分離モードです。対応する Kthena 高度設定が必要です。", + "kthena.form.runtime.mindie": "Ascend 推論ランタイムです。MindIE/Ascend イメージと NPU リソースが必要です。", + "kthena.form.runtime.sglang": "低遅延および複雑な serving シナリオ向けランタイムです。SGLang イメージが必要です。", + "kthena.form.runtime.vllm": "多くの OpenAI 互換モデルサービスに適した汎用 GPU 推論ランタイムです。", + "kthena.form.sections.advanced": "高度なパラメータ", + "kthena.form.sections.env": "環境変数", + "kthena.form.sections.guide": "入力ガイド", + "kthena.form.sections.modelSource": "モデルソース", + "kthena.form.sections.presets": "ランタイムテンプレート", + "kthena.form.sections.resources": "実行パラメータ", + "kthena.form.sections.runtime": "推論サービス", + "kthena.form.sections.scheduling": "スケジューリング制約", + "kthena.form.sections.summary": "デプロイ概要", + "kthena.form.selectNode": "実行ノードを選択", + "kthena.form.selectPlatformModel": "プラットフォームモデルを選択", + "kthena.form.source.external": "URI を手動入力", + "kthena.form.source.platform": "既存のプラットフォームモデル", + "kthena.form.submit": "デプロイ開始", + "kthena.form.summary.applied": "適用済み", + "kthena.form.summary.custom": "カスタム", + "kthena.form.template.delete": "テンプレートを削除", + "kthena.form.template.deleteDescription": "「{{name}}」を削除しますか?既存のモデルデプロイには影響しません。", + "kthena.form.template.deleteTitle": "ランタイムテンプレートを削除", + "kthena.form.template.deleted": "ランタイムテンプレートを削除しました", + "kthena.form.template.description": "テンプレートの説明", + "kthena.form.template.dialogDescription": "モデル、イメージ、リソース、環境変数、高度なパラメータを保存します。現在のアカウントでは本人だけが閲覧できます。", + "kthena.form.template.mine": "マイテンプレート", + "kthena.form.template.name": "テンプレート名", + "kthena.form.template.nameRequired": "テンプレート名を入力してください", + "kthena.form.template.noDescription": "保存したランタイム設定", + "kthena.form.template.save": "テンプレートとして保存", + "kthena.form.template.saveTitle": "ランタイムテンプレートを保存", + "kthena.form.template.saved": "ランタイムテンプレートを保存しました", + "kthena.form.template.update": "テンプレートを更新", + "kthena.form.template.updateTitle": "ランタイムテンプレートを更新", + "kthena.form.title": "新規モデルデプロイ", + "kthena.form.validation.configKeyRequired": "パラメータ名は必須です", + "kthena.form.validation.imageRequired": "イメージアドレスを入力してください", + "kthena.form.validation.modelURIRequired": "モデル URI を入力してください", + "kthena.form.validation.namePattern": "小文字、数字、ハイフンのみ使用できます", + "kthena.form.validation.platformImageRequired": "プラットフォームイメージを選択してください", + "kthena.form.validation.platformModelRequired": "プラットフォームモデルを選択してください", + "kthena.list.description": "Kthena ModelBooster でオンライン推論サービスをデプロイします", + "kthena.list.searchPlaceholder": "デプロイ名を検索", + "kthena.list.showing": "{{count}} 件のデプロイを表示中", + "kthena.list.syncing": "Kthena 状態を同期中", + "kthena.loading": "読み込み中", + "kthena.phase.failed": "失敗", + "kthena.phase.pending": "待機中", + "kthena.phase.progressing": "デプロイ中", + "kthena.phase.ready": "利用可能", + "kthena.phase.unknown": "不明", + "kthena.state.degraded": "異常", + "kthena.state.deploying": "デプロイ中", + "kthena.state.failed": "失敗", + "kthena.state.running": "実行中", + "kthena.state.scheduling": "スケジューリング中", + "kthena.state.submitted": "送信済み", + "kthena.stateDescription.degraded": "BackOff、ヘルスチェック失敗、またはコンテナ異常があります。診断情報を確認してください。", + "kthena.stateDescription.deploying": "Kthena が下流リソースを作成し、推論サービスを起動しています。", + "kthena.stateDescription.failed": "デプロイに失敗しました。診断情報を確認して設定を調整してください。", + "kthena.stateDescription.running": "推論サービスはリクエストを受け付けられる状態です。", + "kthena.stateDescription.scheduling": "リソースは作成済みで、スケジューリングまたは Pod 起動を待っています。", + "kthena.stateDescription.submitted": "デプロイリクエストは送信済みで、Kthena の状態同期を待っています。", + "kthena.summary.attention": "異常", + "kthena.summary.backends": "バックエンド種別", + "kthena.summary.progressing": "デプロイ中", + "kthena.summary.ready": "利用可能", + "kthena.summary.running": "実行中", + "kthena.summary.total": "デプロイ数", + "kthena.table.actions": "操作", + "kthena.table.backend": "バックエンド", + "kthena.table.createdAt": "作成日時", + "kthena.table.image": "イメージ", + "kthena.table.model": "モデル", + "kthena.table.name": "名前", + "kthena.table.owner": "ユーザー", + "kthena.table.replicas": "レプリカ", + "kthena.table.status": "状態", + "kthena.title": "モデルデプロイ", "loadingIndicator.loadingText": "読み込み中...", "loginHeatmap.title": "ユーザーアクティビティ", "modelDownload.action.delete": "削除", @@ -864,6 +1058,7 @@ "navigation.jobTemplates": "ジョブテンプレート", "navigation.jupyterLab": "Jupyter Lab", "navigation.memberManagement": "メンバー管理", + "navigation.modelDeployments": "モデルデプロイ", "navigation.models": "モデル", "navigation.more": "もっと見る", "navigation.myImages": "マイイメージ", @@ -1290,6 +1485,14 @@ "systemConfig.gpuAnalysis.switchLabel": "インテリジェント分析を有効にする", "systemConfig.gpuAnalysis.title": "GPU使用異常分析", "systemConfig.gpuAnalysis.verifyingLLM": "LLMサービスの接続性を検証中...", + "systemConfig.kthenaInference.activeNotice": "モデルデプロイが有効です。ユーザーは Kthena でオンラインサービスを作成できます。", + "systemConfig.kthenaInference.description": "Kthena ベースのオンラインモデルデプロイ、管理、呼び出しを制御します。", + "systemConfig.kthenaInference.disabledSuccess": "モデルデプロイを無効にしました", + "systemConfig.kthenaInference.disabledWarning": "無効にすると、入口は非表示になり、モデルデプロイのページと API への直接アクセスも拒否されます。", + "systemConfig.kthenaInference.enabledSuccess": "モデルデプロイを有効にしました", + "systemConfig.kthenaInference.switchDescription": "有効にすると、ユーザーはオンラインモデルサービスを作成、管理、呼び出しできます。", + "systemConfig.kthenaInference.switchLabel": "モデルデプロイを有効にする", + "systemConfig.kthenaInference.title": "モデルデプロイ", "systemConfig.llm.apiKey": "APIキー", "systemConfig.llm.baseUrl": "APIベースURL", "systemConfig.llm.description": "プラットフォームのインテリジェント分析機能を駆動する言語モデルサービスを設定します。", diff --git a/frontend/src/i18n/locales/ko/translation.json b/frontend/src/i18n/locales/ko/translation.json index 17a89895a..38f1c3ae1 100644 --- a/frontend/src/i18n/locales/ko/translation.json +++ b/frontend/src/i18n/locales/ko/translation.json @@ -686,6 +686,200 @@ "jupyter.snapshot.save": "저장", "jupyter.snapshot.success": "스냅샷 제출 완료", "jupyter.snapshot.title": "이미지 저장", + "kthena.actions.back": "뒤로", + "kthena.actions.backToList": "목록으로 돌아가기", + "kthena.actions.cancel": "취소", + "kthena.actions.clearChat": "대화 지우기", + "kthena.actions.clone": "배포 복제", + "kthena.actions.create": "새 배포", + "kthena.actions.delete": "삭제", + "kthena.actions.deleteDeployment": "배포 삭제", + "kthena.actions.more": "더 많은 작업", + "kthena.actions.requesting": "요청 중", + "kthena.actions.send": "요청 보내기", + "kthena.actions.view": "상세 보기", + "kthena.chat.collapseSessions": "대화 목록 접기", + "kthena.chat.deleteSession": "대화 삭제", + "kthena.chat.emptyTitle": "새 대화 시작", + "kthena.chat.expandSessions": "대화 목록 펼치기", + "kthena.chat.newSession": "새 대화", + "kthena.chat.sessions": "대화 목록", + "kthena.chat.untitledSession": "제목 없는 대화", + "kthena.copy.generic": "{{label}} 복사됨", + "kthena.copy.image": "이미지 주소가 복사되었습니다", + "kthena.copy.model": "모델 이름이 복사되었습니다", + "kthena.copy.resource": "리소스 이름이 복사되었습니다", + "kthena.delete.description": "Kthena ModelBooster {{name}} 및 관련 워크로드를 삭제합니다.", + "kthena.delete.success": "모델 배포가 삭제되었습니다", + "kthena.delete.title": "모델 배포 삭제", + "kthena.detail.apiBase": "API 엔드포인트", + "kthena.detail.curlContext": "현재 대화 컨텍스트", + "kthena.detail.defaultPrompt": "Crater와 Kthena가 모델 서비스를 제공하기 위해 어떻게 협력하는지 한 문장으로 설명하세요.", + "kthena.detail.description": "Kthena ModelBooster 온라인 추론 서비스", + "kthena.detail.diagnosticsTitle": "진단", + "kthena.detail.invokeHint": "CLI 호출에는 Crater 로그인 토큰이 필요합니다. 웹 테스트는 현재 로그인 세션을 자동으로 사용합니다.", + "kthena.detail.invokeInfo": "호출 정보", + "kthena.detail.logDetails": "컨테이너 로그 상세", + "kthena.detail.modelName": "모델 이름", + "kthena.detail.noDiagnostics": "배포를 막는 Pod, 이벤트 또는 컨테이너 오류가 발견되지 않았습니다.", + "kthena.detail.noResources": "관련 리소스가 없습니다", + "kthena.detail.onlineTest": "온라인 테스트", + "kthena.detail.overviewTitle": "서비스 개요", + "kthena.detail.primaryIssue": "주요 문제", + "kthena.detail.promptPlaceholder": "이 모델 배포에 메시지 보내기", + "kthena.detail.rawResponse": "원본 응답", + "kthena.detail.responsePlaceholder": "응답이 여기에 표시됩니다", + "kthena.detail.restarts": "{{count}}회 재시작", + "kthena.detail.routeModelName": "라우트 모델 이름", + "kthena.detail.routeResource": "라우트 리소스", + "kthena.detail.runtimeNode": "런타임 노드", + "kthena.detail.runtimePod": "런타임 Pod", + "kthena.detail.runtimePods": "런타임 Pod", + "kthena.detail.runtimeResources": "런타임 리소스", + "kthena.detail.servedModel": "서빙 모델", + "kthena.detail.tabs.diagnostics": "진단", + "kthena.detail.tabs.invoke": "호출", + "kthena.detail.tabs.overview": "개요", + "kthena.detail.tabs.resources": "Kthena 리소스", + "kthena.detail.tabs.usage": "리소스 사용량", + "kthena.detail.title": "모델 배포 상세 정보", + "kthena.detail.usageDescription": "선택한 런타임 Pod의 최근 15분 CPU, 메모리 및 가속기 실시간 지표를 표시합니다.", + "kthena.detail.usageNoGrafana": "현재 환경에 Grafana Pod 모니터가 구성되어 있지 않아 실시간 리소스 사용량을 표시할 수 없습니다.", + "kthena.detail.usageNoPod": "모니터링할 런타임 Pod가 아직 없습니다. 배포가 실행되면 여기에 지표가 표시됩니다.", + "kthena.detail.usageNotScheduled": "런타임 Pod가 아직 노드에 스케줄되지 않았습니다. 스케줄링 후 리소스 사용량을 확인할 수 있습니다.", + "kthena.detail.usageTitle": "리소스 사용량", + "kthena.detail.usageUnavailable": "리소스 사용량을 사용할 수 없음", + "kthena.detail.waitingModelRoute": "ModelRoute 대기 중", + "kthena.empty": "모델 배포가 없습니다", + "kthena.filter.all": "전체", + "kthena.filter.attention": "이상", + "kthena.filter.progressing": "진행 중", + "kthena.filter.ready": "사용 가능", + "kthena.filter.running": "실행 중", + "kthena.form.actions.addConfig": "파라미터 추가", + "kthena.form.cloneDescription": "{{name}}의 설정을 복사하여 새 배포를 생성합니다", + "kthena.form.createSuccess": "모델 배포 {{name}}이 생성되었습니다", + "kthena.form.description": "플랫폼 모델을 우선 사용하고, 외부 저장소가 필요할 때만 수동 URI로 전환하세요", + "kthena.form.descriptions.cacheURI": "Kthena가 외부 모델을 다운로드한 뒤 저장하는 위치입니다. 기본값은 hostpath:///tmp/cache입니다. 운영 환경에서는 별도 디스크 또는 PVC 사용을 권장합니다.", + "kthena.form.descriptions.gpuModel": "GPU 모델은 Kubernetes 리소스 이름으로 Kthena worker limits에 기록됩니다. V100/A100/국산 GPU 리소스 이름을 선택하면 해당 리소스를 가진 노드만 선택됩니다. 특정 GPU 모델을 지정하지 않으면 플랫폼 리소스 정의에 따라 배치됩니다.", + "kthena.form.descriptions.imageSource": "플랫폼 이미지를 우선 선택하세요. 추론 백엔드를 전환하면 권장 이미지 주소가 입력되며, 이후 플랫폼 이미지로 다시 바꿀 수 있습니다.", + "kthena.form.descriptions.modelURI": "hf://, s3://, pvc://, ms://를 지원합니다", + "kthena.form.descriptions.node": "특정 머신 디버깅, 부하 테스트 또는 리소스 단편화 회피에 사용합니다. 기본값은 스케줄러가 노드를 선택합니다.", + "kthena.form.descriptions.pinNode": "활성화하면 Kthena가 생성한 추론 Pod가 선택한 노드에만 스케줄됩니다.", + "kthena.form.descriptions.platformModel": "현재 계정에서 접근 가능한 모델 리소스가 표시됩니다", + "kthena.form.descriptions.selectedModel": "{{url}}을 사용합니다", + "kthena.form.descriptions.servedModel": "OpenAI 요청의 model 이름입니다. 플랫폼 모델은 기본적으로 모델 리소스 이름을 사용합니다.", + "kthena.form.descriptions.serviceName": "Kthena ModelBooster 이름 생성에 사용됩니다", + "kthena.form.descriptions.source": "플랫폼 모델은 Kthena가 지원하는 pvc:// 경로로 변환됩니다. 외부 URI는 hf://, s3://, ms://, pvc://를 지원합니다.", + "kthena.form.descriptions.workerConfig": "worker.config는 Kthena 정의 필드이며 추론 엔진 시작 파라미터로 변환됩니다. 환경 변수는 컨테이너 환경에 주입됩니다. 두 설정의 용도는 다릅니다.", + "kthena.form.descriptions.workerImage": "실제 추론 런타임을 시작하는 컨테이너 이미지입니다. 선택한 추론 백엔드와 일치해야 합니다.", + "kthena.form.fields.backend": "추론 백엔드", + "kthena.form.fields.cacheURI": "모델 캐시 URI", + "kthena.form.fields.configKey": "파라미터 이름", + "kthena.form.fields.configValue": "파라미터 값", + "kthena.form.fields.imageSource": "이미지 소스", + "kthena.form.fields.modelURI": "모델 URI", + "kthena.form.fields.node": "노드", + "kthena.form.fields.pinNode": "실행 노드 지정", + "kthena.form.fields.platformModel": "플랫폼 모델", + "kthena.form.fields.replicas": "레플리카 수", + "kthena.form.fields.servedModel": "외부 모델 이름", + "kthena.form.fields.serviceName": "서비스 이름", + "kthena.form.fields.source": "소스", + "kthena.form.fields.workerImage": "Worker 이미지", + "kthena.form.guide.advanced": "고급 파라미터: Kthena worker.config에 기록되며 최종적으로 vLLM/SGLang 등의 추론 엔진 시작 파라미터가 됩니다.", + "kthena.form.guide.cache": "모델 캐시 URI: 외부 모델 다운로드 후 캐시 위치입니다. 플랫폼 모델은 일반적으로 변경할 필요가 없습니다.", + "kthena.form.guide.env": "환경 변수: 컨테이너 환경에 주입되며 HF_ENDPOINT, ENDPOINT, NCCL_IB_DISABLE 등에 자주 사용됩니다.", + "kthena.form.guide.model": "모델 서비스: 플랫폼 모델은 Crater에 등록된 모델 경로를 사용합니다. 외부 모델만 수동 URI가 필요합니다.", + "kthena.form.guide.resources": "실행 리소스: CPU/메모리/GPU는 작업 제출과 동일합니다. GPU 수가 0보다 크면 GPU 모델을 선택해야 합니다.", + "kthena.form.imageSource.manual": "수동 이미지 주소", + "kthena.form.imageSource.platform": "플랫폼 이미지", + "kthena.form.preset.cpu.description": "CPU 리소스, vLLM 백엔드 및 기본 파라미터를 적용합니다. 모델은 나중에 플랫폼 모델로 변경할 수 있습니다.", + "kthena.form.preset.cpu.title": "CPU 소형 모델", + "kthena.form.preset.gpu.description": "GPU 리소스, vLLM 이미지 및 일반적인 처리량 파라미터를 적용합니다", + "kthena.form.preset.sglang.description": "SGLang 백엔드, SGLang 이미지 및 컨텍스트 파라미터를 적용합니다", + "kthena.form.preset.sglang.title": "SGLang 서비스", + "kthena.form.runtime.default": "Kthena가 시작할 추론 런타임 종류를 결정합니다.", + "kthena.form.runtime.disaggregated": "vLLM prefill/decode 분리 모드입니다. 관련 Kthena 고급 설정이 필요합니다.", + "kthena.form.runtime.mindie": "Ascend 추론 런타임입니다. MindIE/Ascend 이미지와 NPU 리소스가 필요합니다.", + "kthena.form.runtime.sglang": "저지연 및 복잡한 serving 시나리오용 런타임입니다. SGLang 이미지가 필요합니다.", + "kthena.form.runtime.vllm": "대부분의 OpenAI 호환 모델 서비스에 적합한 범용 GPU 추론 런타임입니다.", + "kthena.form.sections.advanced": "고급 파라미터", + "kthena.form.sections.env": "환경 변수", + "kthena.form.sections.guide": "입력 안내", + "kthena.form.sections.modelSource": "모델 소스", + "kthena.form.sections.presets": "런타임 템플릿", + "kthena.form.sections.resources": "실행 파라미터", + "kthena.form.sections.runtime": "추론 서비스", + "kthena.form.sections.scheduling": "스케줄링 제약", + "kthena.form.sections.summary": "배포 요약", + "kthena.form.selectNode": "실행 노드 선택", + "kthena.form.selectPlatformModel": "플랫폼 모델 선택", + "kthena.form.source.external": "URI 직접 입력", + "kthena.form.source.platform": "기존 플랫폼 모델", + "kthena.form.submit": "배포 시작", + "kthena.form.summary.applied": "적용됨", + "kthena.form.summary.custom": "사용자 지정", + "kthena.form.template.delete": "템플릿 삭제", + "kthena.form.template.deleteDescription": "“{{name}}” 템플릿을 삭제할까요? 이미 생성된 모델 배포에는 영향을 주지 않습니다.", + "kthena.form.template.deleteTitle": "런타임 템플릿 삭제", + "kthena.form.template.deleted": "런타임 템플릿이 삭제되었습니다", + "kthena.form.template.description": "템플릿 설명", + "kthena.form.template.dialogDescription": "모델, 이미지, 리소스, 환경 변수 및 고급 파라미터를 저장합니다. 현재 계정에서는 본인에게만 표시됩니다.", + "kthena.form.template.mine": "내 템플릿", + "kthena.form.template.name": "템플릿 이름", + "kthena.form.template.nameRequired": "템플릿 이름을 입력하세요", + "kthena.form.template.noDescription": "저장된 런타임 구성", + "kthena.form.template.save": "템플릿으로 저장", + "kthena.form.template.saveTitle": "런타임 템플릿 저장", + "kthena.form.template.saved": "런타임 템플릿이 저장되었습니다", + "kthena.form.template.update": "템플릿 업데이트", + "kthena.form.template.updateTitle": "런타임 템플릿 업데이트", + "kthena.form.title": "새 모델 배포", + "kthena.form.validation.configKeyRequired": "파라미터 이름은 필수입니다", + "kthena.form.validation.imageRequired": "이미지 주소를 입력하세요", + "kthena.form.validation.modelURIRequired": "모델 URI를 입력하세요", + "kthena.form.validation.namePattern": "소문자, 숫자 및 하이픈만 사용할 수 있습니다", + "kthena.form.validation.platformImageRequired": "플랫폼 이미지를 선택하세요", + "kthena.form.validation.platformModelRequired": "플랫폼 모델을 선택하세요", + "kthena.list.description": "Kthena ModelBooster로 온라인 추론 서비스를 배포합니다", + "kthena.list.searchPlaceholder": "배포 이름 검색", + "kthena.list.showing": "{{count}}개의 배포 표시 중", + "kthena.list.syncing": "Kthena 상태 동기화 중", + "kthena.loading": "로딩 중", + "kthena.phase.failed": "실패", + "kthena.phase.pending": "대기 중", + "kthena.phase.progressing": "배포 중", + "kthena.phase.ready": "사용 가능", + "kthena.phase.unknown": "알 수 없음", + "kthena.state.degraded": "이상", + "kthena.state.deploying": "배포 중", + "kthena.state.failed": "실패", + "kthena.state.running": "실행 중", + "kthena.state.scheduling": "스케줄링 중", + "kthena.state.submitted": "제출됨", + "kthena.stateDescription.degraded": "BackOff, 헬스 체크 실패 또는 컨테이너 문제가 있습니다. 진단 정보를 확인하세요.", + "kthena.stateDescription.deploying": "Kthena가 하위 리소스를 만들고 추론 서비스를 시작하고 있습니다.", + "kthena.stateDescription.failed": "배포에 실패했습니다. 진단 정보를 확인하고 설정을 조정하세요.", + "kthena.stateDescription.running": "추론 서비스가 요청을 받을 준비가 되었습니다.", + "kthena.stateDescription.scheduling": "리소스가 생성되었으며 스케줄링 또는 Pod 시작을 기다리고 있습니다.", + "kthena.stateDescription.submitted": "배포 요청이 제출되었으며 Kthena 상태 동기화를 기다리고 있습니다.", + "kthena.summary.attention": "이상", + "kthena.summary.backends": "백엔드 유형", + "kthena.summary.progressing": "배포 중", + "kthena.summary.ready": "사용 가능", + "kthena.summary.running": "실행 중", + "kthena.summary.total": "배포 수", + "kthena.table.actions": "작업", + "kthena.table.backend": "백엔드", + "kthena.table.createdAt": "생성 시간", + "kthena.table.image": "이미지", + "kthena.table.model": "모델", + "kthena.table.name": "이름", + "kthena.table.owner": "사용자", + "kthena.table.replicas": "레플리카", + "kthena.table.status": "상태", + "kthena.title": "모델 배포", "loadingIndicator.loadingText": "로딩 중...", "loginHeatmap.title": "사용자 활동도", "modelDownload.action.delete": "삭제", @@ -861,6 +1055,7 @@ "navigation.jobTemplates": "작업 템플릿", "navigation.jupyterLab": "Jupyter Lab", "navigation.memberManagement": "멤버 관리", + "navigation.modelDeployments": "모델 배포", "navigation.models": "모델", "navigation.more": "더 보기", "navigation.myImages": "내 이미지", @@ -1287,6 +1482,14 @@ "systemConfig.gpuAnalysis.switchLabel": "지능형 분석 활성화", "systemConfig.gpuAnalysis.title": "GPU 사용 이상 분석", "systemConfig.gpuAnalysis.verifyingLLM": "LLM 서비스 연결을 확인하는 중...", + "systemConfig.kthenaInference.activeNotice": "모델 배포가 활성화되었습니다. 사용자는 Kthena로 온라인 서비스를 만들 수 있습니다.", + "systemConfig.kthenaInference.description": "Kthena 기반 온라인 모델 배포, 관리 및 호출 기능을 제어합니다.", + "systemConfig.kthenaInference.disabledSuccess": "모델 배포 기능이 비활성화되었습니다", + "systemConfig.kthenaInference.disabledWarning": "비활성화하면 메뉴가 숨겨지고 모델 배포 페이지와 API 직접 접근도 거부됩니다.", + "systemConfig.kthenaInference.enabledSuccess": "모델 배포 기능이 활성화되었습니다", + "systemConfig.kthenaInference.switchDescription": "활성화하면 사용자가 온라인 모델 서비스를 생성, 관리, 호출할 수 있습니다.", + "systemConfig.kthenaInference.switchLabel": "모델 배포 활성화", + "systemConfig.kthenaInference.title": "모델 배포", "systemConfig.llm.apiKey": "API 키", "systemConfig.llm.baseUrl": "API 기본 URL", "systemConfig.llm.description": "플랫폼의 지능형 분석 기능을 구동하는 언어 모델 서비스를 구성합니다.", diff --git a/frontend/src/i18n/locales/zhCN/translation.json b/frontend/src/i18n/locales/zhCN/translation.json index ee03621f9..fa3f677be 100644 --- a/frontend/src/i18n/locales/zhCN/translation.json +++ b/frontend/src/i18n/locales/zhCN/translation.json @@ -687,6 +687,200 @@ "jupyter.snapshot.save": "保存", "jupyter.snapshot.success": "已提交快照", "jupyter.snapshot.title": "保存镜像", + "kthena.actions.back": "返回", + "kthena.actions.backToList": "返回列表", + "kthena.actions.cancel": "取消", + "kthena.actions.clearChat": "清空对话", + "kthena.actions.clone": "复制部署", + "kthena.actions.create": "新建部署", + "kthena.actions.delete": "删除", + "kthena.actions.deleteDeployment": "删除部署", + "kthena.actions.more": "更多操作", + "kthena.actions.requesting": "请求中", + "kthena.actions.send": "发送请求", + "kthena.actions.view": "查看详情", + "kthena.chat.collapseSessions": "收起会话列表", + "kthena.chat.deleteSession": "删除会话", + "kthena.chat.emptyTitle": "开始一段新对话", + "kthena.chat.expandSessions": "展开会话列表", + "kthena.chat.newSession": "新建会话", + "kthena.chat.sessions": "会话列表", + "kthena.chat.untitledSession": "未命名会话", + "kthena.copy.generic": "{{label}} 已复制", + "kthena.copy.image": "镜像地址已复制", + "kthena.copy.model": "模型名称已复制", + "kthena.copy.resource": "资源名称已复制", + "kthena.delete.description": "将删除 Kthena ModelBooster {{name}} 及其关联工作负载。", + "kthena.delete.success": "模型部署已删除", + "kthena.delete.title": "删除模型部署", + "kthena.detail.apiBase": "调用地址", + "kthena.detail.curlContext": "当前会话上下文", + "kthena.detail.defaultPrompt": "用一句话介绍 Crater 和 Kthena 如何协同提供模型服务。", + "kthena.detail.description": "Kthena ModelBooster 在线推理服务", + "kthena.detail.diagnosticsTitle": "诊断信息", + "kthena.detail.invokeHint": "命令行调用需要带 Crater 登录 token;网页在线验证会自动携带当前登录态。", + "kthena.detail.invokeInfo": "调用信息", + "kthena.detail.logDetails": "容器日志详情", + "kthena.detail.modelName": "模型名", + "kthena.detail.noDiagnostics": "暂未发现阻塞部署的 Pod、事件或容器错误。", + "kthena.detail.noResources": "暂未发现关联资源", + "kthena.detail.onlineTest": "在线验证", + "kthena.detail.overviewTitle": "服务概览", + "kthena.detail.primaryIssue": "首要问题", + "kthena.detail.promptPlaceholder": "向这个模型部署发送消息", + "kthena.detail.rawResponse": "原始响应", + "kthena.detail.responsePlaceholder": "响应会显示在这里", + "kthena.detail.restarts": "重启 {{count}} 次", + "kthena.detail.routeModelName": "路由模型名", + "kthena.detail.routeResource": "路由资源", + "kthena.detail.runtimeNode": "运行节点", + "kthena.detail.runtimePod": "运行 Pod", + "kthena.detail.runtimePods": "运行 Pod", + "kthena.detail.runtimeResources": "运行资源", + "kthena.detail.servedModel": "服务模型", + "kthena.detail.tabs.diagnostics": "诊断", + "kthena.detail.tabs.invoke": "调用", + "kthena.detail.tabs.overview": "概览", + "kthena.detail.tabs.resources": "Kthena 资源", + "kthena.detail.tabs.usage": "资源占用", + "kthena.detail.title": "模型部署详情", + "kthena.detail.usageDescription": "展示所选运行 Pod 最近 15 分钟的 CPU、内存与加速卡实时指标。", + "kthena.detail.usageNoGrafana": "当前环境未配置 Grafana Pod 监控地址,暂时无法展示实时资源占用。", + "kthena.detail.usageNoPod": "当前还没有可用于查看资源占用的运行 Pod。部署完成并进入运行状态后,监控数据会显示在这里。", + "kthena.detail.usageNotScheduled": "运行 Pod 尚未调度到节点,等待调度完成后即可查看实时资源占用。", + "kthena.detail.usageTitle": "资源占用", + "kthena.detail.usageUnavailable": "资源占用暂不可用", + "kthena.detail.waitingModelRoute": "等待 ModelRoute", + "kthena.empty": "暂无模型部署", + "kthena.filter.all": "全部", + "kthena.filter.attention": "异常", + "kthena.filter.progressing": "进行中", + "kthena.filter.ready": "可用", + "kthena.filter.running": "运行中", + "kthena.form.actions.addConfig": "添加参数", + "kthena.form.cloneDescription": "复制 {{name}} 的配置后创建新部署", + "kthena.form.createSuccess": "模型部署 {{name}} 已创建", + "kthena.form.description": "优先使用平台模型;需要外部仓库时再切换到手动 URI", + "kthena.form.descriptions.cacheURI": "Kthena 下载外部模型后存放的位置。默认 hostpath:///tmp/cache;生产环境建议使用独立大盘或 PVC。", + "kthena.form.descriptions.gpuModel": "GPU 卡型会作为 Kubernetes 资源名写入 Kthena worker limits。选择 V100/A100/国产 GPU 对应资源名后,调度器只会选择具备该资源的节点;不指定具体卡型时由平台资源定义决定可落节点范围。", + "kthena.form.descriptions.imageSource": "优先选择平台镜像;切换推理后端时会填入推荐镜像地址,可再改回平台镜像。", + "kthena.form.descriptions.modelURI": "支持 hf://、s3://、pvc://、ms://", + "kthena.form.descriptions.node": "需要指定具体机器调试、压测或规避资源碎片时使用;默认交给调度器选择。", + "kthena.form.descriptions.pinNode": "打开后,Kthena 生成的推理 Pod 只会调度到选中的节点。", + "kthena.form.descriptions.platformModel": "这里列出当前账号可访问的模型资源", + "kthena.form.descriptions.selectedModel": "将使用 {{url}}", + "kthena.form.descriptions.servedModel": "OpenAI 请求里的 model 名称;平台模型默认使用模型资源名。", + "kthena.form.descriptions.serviceName": "用于生成 Kthena ModelBooster 名称", + "kthena.form.descriptions.source": "平台模型会转换为 Kthena 支持的 pvc:// 路径;外部 URI 支持 hf://、s3://、ms://、pvc://。", + "kthena.form.descriptions.workerConfig": "worker.config 是 Kthena 的定义字段,会被转换成推理引擎启动参数;环境变量则是写入容器环境。二者作用不同。", + "kthena.form.descriptions.workerImage": "实际启动推理运行时的容器镜像;需要和推理后端匹配。", + "kthena.form.fields.backend": "推理后端", + "kthena.form.fields.cacheURI": "模型缓存 URI", + "kthena.form.fields.configKey": "参数名", + "kthena.form.fields.configValue": "参数值", + "kthena.form.fields.imageSource": "镜像来源", + "kthena.form.fields.modelURI": "模型 URI", + "kthena.form.fields.node": "节点", + "kthena.form.fields.pinNode": "指定运行节点", + "kthena.form.fields.platformModel": "平台模型", + "kthena.form.fields.replicas": "副本数", + "kthena.form.fields.servedModel": "对外模型名", + "kthena.form.fields.serviceName": "服务名称", + "kthena.form.fields.source": "来源", + "kthena.form.fields.workerImage": "Worker 镜像", + "kthena.form.guide.advanced": "高级参数:写入 Kthena worker.config,最终成为 vLLM/SGLang 等推理引擎启动参数。", + "kthena.form.guide.cache": "模型缓存 URI:用于外部模型下载后的缓存位置,平台已有模型一般不需要改。", + "kthena.form.guide.env": "环境变量:写入容器环境,常用于 HF_ENDPOINT、ENDPOINT、NCCL_IB_DISABLE 等。", + "kthena.form.guide.model": "模型服务:选择平台模型时会使用 Crater 已登记的模型路径;只有外部模型需要手动写 URI。", + "kthena.form.guide.resources": "运行参数:CPU/内存/GPU 和作业提交一致;GPU 数量大于 0 时必须选择卡型。", + "kthena.form.imageSource.manual": "手动镜像地址", + "kthena.form.imageSource.platform": "平台已有镜像", + "kthena.form.preset.cpu.description": "套用 CPU 资源、vLLM 后端和基础参数;模型仍可改成平台模型", + "kthena.form.preset.cpu.title": "CPU 小模型", + "kthena.form.preset.gpu.description": "套用 GPU 资源、vLLM 镜像和常用吞吐参数", + "kthena.form.preset.sglang.description": "套用 SGLang 后端、SGLang 镜像和上下文参数", + "kthena.form.preset.sglang.title": "SGLang 服务", + "kthena.form.runtime.default": "决定 Kthena 启动哪类推理运行时。", + "kthena.form.runtime.disaggregated": "vLLM 预填充/解码分离模式,需要配套的 Kthena 高级配置。", + "kthena.form.runtime.mindie": "昇腾推理运行时,需要匹配 MindIE/Ascend 镜像和 NPU 资源。", + "kthena.form.runtime.sglang": "面向低延迟和复杂 serving 场景的运行时,需要使用 SGLang 镜像。", + "kthena.form.runtime.vllm": "通用 GPU 推理运行时,适合大多数 OpenAI 兼容模型服务。", + "kthena.form.sections.advanced": "高级参数", + "kthena.form.sections.env": "环境变量", + "kthena.form.sections.guide": "填写说明", + "kthena.form.sections.modelSource": "模型来源", + "kthena.form.sections.presets": "运行时模板", + "kthena.form.sections.resources": "运行参数", + "kthena.form.sections.runtime": "推理服务", + "kthena.form.sections.scheduling": "调度约束", + "kthena.form.sections.summary": "部署摘要", + "kthena.form.selectNode": "选择运行节点", + "kthena.form.selectPlatformModel": "选择平台模型", + "kthena.form.source.external": "手动填写 URI", + "kthena.form.source.platform": "平台已有模型", + "kthena.form.submit": "开始部署", + "kthena.form.summary.applied": "已套用", + "kthena.form.summary.custom": "自定义", + "kthena.form.template.delete": "删除模板", + "kthena.form.template.deleteDescription": "确定删除“{{name}}”吗?此操作不会影响已创建的模型部署。", + "kthena.form.template.deleteTitle": "删除运行模板", + "kthena.form.template.deleted": "运行模板已删除", + "kthena.form.template.description": "模板说明", + "kthena.form.template.dialogDescription": "保存模型、镜像、资源、环境变量和高级参数;仅当前用户在当前账户可见。", + "kthena.form.template.mine": "我的模板", + "kthena.form.template.name": "模板名称", + "kthena.form.template.nameRequired": "请填写模板名称", + "kthena.form.template.noDescription": "保存当前运行配置", + "kthena.form.template.save": "保存为模板", + "kthena.form.template.saveTitle": "保存为运行模板", + "kthena.form.template.saved": "运行模板已保存", + "kthena.form.template.update": "更新模板", + "kthena.form.template.updateTitle": "更新运行模板", + "kthena.form.title": "新建模型部署", + "kthena.form.validation.configKeyRequired": "参数名不能为空", + "kthena.form.validation.imageRequired": "请输入镜像地址", + "kthena.form.validation.modelURIRequired": "请输入模型 URI", + "kthena.form.validation.namePattern": "只能包含小写字母、数字和连字符", + "kthena.form.validation.platformImageRequired": "请选择平台镜像", + "kthena.form.validation.platformModelRequired": "请选择平台模型", + "kthena.list.description": "通过 Kthena ModelBooster 部署在线推理服务", + "kthena.list.searchPlaceholder": "搜索部署名称", + "kthena.list.showing": "当前显示 {{count}} 个部署", + "kthena.list.syncing": "正在同步 Kthena 状态", + "kthena.loading": "正在加载", + "kthena.phase.failed": "失败", + "kthena.phase.pending": "等待中", + "kthena.phase.progressing": "部署中", + "kthena.phase.ready": "可用", + "kthena.phase.unknown": "未知", + "kthena.state.degraded": "异常", + "kthena.state.deploying": "部署中", + "kthena.state.failed": "失败", + "kthena.state.running": "运行中", + "kthena.state.scheduling": "排队中", + "kthena.state.submitted": "已提交", + "kthena.stateDescription.degraded": "部署存在 BackOff、健康检查失败或容器异常,请查看诊断信息。", + "kthena.stateDescription.deploying": "Kthena 正在创建下游资源并启动推理服务。", + "kthena.stateDescription.failed": "部署失败,需要查看诊断信息并调整配置。", + "kthena.stateDescription.running": "推理服务已经就绪,可以接收请求。", + "kthena.stateDescription.scheduling": "资源已创建,正在等待调度或 Pod 启动。", + "kthena.stateDescription.submitted": "部署请求已提交,正在等待 Kthena 同步状态。", + "kthena.summary.attention": "异常", + "kthena.summary.backends": "后端类型", + "kthena.summary.progressing": "部署中", + "kthena.summary.ready": "可用", + "kthena.summary.running": "运行中", + "kthena.summary.total": "部署总数", + "kthena.table.actions": "操作", + "kthena.table.backend": "后端", + "kthena.table.createdAt": "创建时间", + "kthena.table.image": "镜像", + "kthena.table.model": "模型", + "kthena.table.name": "名称", + "kthena.table.owner": "用户", + "kthena.table.replicas": "副本", + "kthena.table.status": "状态", + "kthena.title": "模型部署", "loadingIndicator.loadingText": "加载中...", "loginHeatmap.title": "用户活跃度", "modelDownload.action.delete": "删除", @@ -864,6 +1058,7 @@ "navigation.jobTemplates": "作业模板", "navigation.jupyterLab": "Jupyter Lab", "navigation.memberManagement": "成员管理", + "navigation.modelDeployments": "模型部署", "navigation.models": "模型", "navigation.more": "更多", "navigation.myImages": "我的镜像", @@ -1290,6 +1485,14 @@ "systemConfig.gpuAnalysis.switchLabel": "开启智能分析", "systemConfig.gpuAnalysis.title": "GPU 占卡异常分析", "systemConfig.gpuAnalysis.verifyingLLM": "正在验证 LLM 服务连通性...", + "systemConfig.kthenaInference.activeNotice": "模型部署已开启,用户可以使用 Kthena 创建在线模型服务。", + "systemConfig.kthenaInference.description": "控制基于 Kthena 的在线模型部署、管理与调用能力。", + "systemConfig.kthenaInference.disabledSuccess": "模型部署功能已关闭", + "systemConfig.kthenaInference.disabledWarning": "功能关闭后将隐藏入口,直接访问模型部署页面和 API 也会被拒绝。", + "systemConfig.kthenaInference.enabledSuccess": "模型部署功能已开启", + "systemConfig.kthenaInference.switchDescription": "开启后,用户可以创建、管理和调用在线模型服务。", + "systemConfig.kthenaInference.switchLabel": "启用模型部署", + "systemConfig.kthenaInference.title": "模型部署", "systemConfig.llm.apiKey": "API 密钥", "systemConfig.llm.baseUrl": "API 基础地址", "systemConfig.llm.description": "配置用于驱动平台智能分析功能的语言模型服务。", diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index c2bf14153..278757ae0 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -19,6 +19,7 @@ import { Route as PortalUsersRouteRouteImport } from './routes/portal/users/rout import { Route as PortalTemplatesRouteRouteImport } from './routes/portal/templates/route' import { Route as PortalOverviewRouteRouteImport } from './routes/portal/overview/route' import { Route as PortalMoreRouteRouteImport } from './routes/portal/more/route' +import { Route as PortalInferenceServicesRouteRouteImport } from './routes/portal/inference-services/route' import { Route as PortalAccountRouteRouteImport } from './routes/portal/account/route' import { Route as AdminUsersRouteRouteImport } from './routes/admin/users/route' import { Route as AdminStatisticsRouteRouteImport } from './routes/admin/statistics/route' @@ -30,6 +31,7 @@ import { Route as PortalUsersIndexRouteImport } from './routes/portal/users/inde import { Route as PortalTemplatesIndexRouteImport } from './routes/portal/templates/index' import { Route as PortalOverviewIndexRouteImport } from './routes/portal/overview/index' import { Route as PortalMoreIndexRouteImport } from './routes/portal/more/index' +import { Route as PortalInferenceServicesIndexRouteImport } from './routes/portal/inference-services/index' import { Route as AdminUsersIndexRouteImport } from './routes/admin/users/index' import { Route as AdminStatisticsIndexRouteImport } from './routes/admin/statistics/index' import { Route as AdminOperationLogsIndexRouteImport } from './routes/admin/operation-logs/index' @@ -46,6 +48,8 @@ import { Route as PortalMoreUserRouteImport } from './routes/portal/more/user' import { Route as PortalMonitorNetworkRouteImport } from './routes/portal/monitor/network' import { Route as PortalMonitorIdleRouteImport } from './routes/portal/monitor/idle' import { Route as PortalMonitorGpuRouteImport } from './routes/portal/monitor/gpu' +import { Route as PortalInferenceServicesNewRouteImport } from './routes/portal/inference-services/new' +import { Route as PortalInferenceServicesNameRouteImport } from './routes/portal/inference-services/$name' import { Route as PortalFilesSplatRouteImport } from './routes/portal/files/$' import { Route as PortalAccountStatisticsRouteImport } from './routes/portal/account/statistics' import { Route as PortalAccountMemberRouteImport } from './routes/portal/account/member' @@ -156,6 +160,12 @@ const PortalMoreRouteRoute = PortalMoreRouteRouteImport.update({ path: '/more', getParentRoute: () => PortalRouteRoute, } as any) +const PortalInferenceServicesRouteRoute = + PortalInferenceServicesRouteRouteImport.update({ + id: '/inference-services', + path: '/inference-services', + getParentRoute: () => PortalRouteRoute, + } as any) const PortalAccountRouteRoute = PortalAccountRouteRouteImport.update({ id: '/account', path: '/account', @@ -211,6 +221,12 @@ const PortalMoreIndexRoute = PortalMoreIndexRouteImport.update({ path: '/', getParentRoute: () => PortalMoreRouteRoute, } as any) +const PortalInferenceServicesIndexRoute = + PortalInferenceServicesIndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => PortalInferenceServicesRouteRoute, + } as any) const AdminUsersIndexRoute = AdminUsersIndexRouteImport.update({ id: '/', path: '/', @@ -291,6 +307,18 @@ const PortalMonitorGpuRoute = PortalMonitorGpuRouteImport.update({ path: '/monitor/gpu', getParentRoute: () => PortalRouteRoute, } as any) +const PortalInferenceServicesNewRoute = + PortalInferenceServicesNewRouteImport.update({ + id: '/new', + path: '/new', + getParentRoute: () => PortalInferenceServicesRouteRoute, + } as any) +const PortalInferenceServicesNameRoute = + PortalInferenceServicesNameRouteImport.update({ + id: '/$name', + path: '/$name', + getParentRoute: () => PortalInferenceServicesRouteRoute, + } as any) const PortalFilesSplatRoute = PortalFilesSplatRouteImport.update({ id: '/files/$', path: '/files/$', @@ -607,6 +635,7 @@ export interface FileRoutesByFullPath { '/admin/statistics': typeof AdminStatisticsRouteRouteWithChildren '/admin/users': typeof AdminUsersRouteRouteWithChildren '/portal/account': typeof PortalAccountRouteRouteWithChildren + '/portal/inference-services': typeof PortalInferenceServicesRouteRouteWithChildren '/portal/more': typeof PortalMoreRouteRouteWithChildren '/portal/overview': typeof PortalOverviewRouteRouteWithChildren '/portal/templates': typeof PortalTemplatesRouteRouteWithChildren @@ -639,6 +668,8 @@ export interface FileRoutesByFullPath { '/portal/account/member': typeof PortalAccountMemberRoute '/portal/account/statistics': typeof PortalAccountStatisticsRoute '/portal/files/$': typeof PortalFilesSplatRoute + '/portal/inference-services/$name': typeof PortalInferenceServicesNameRoute + '/portal/inference-services/new': typeof PortalInferenceServicesNewRoute '/portal/monitor/gpu': typeof PortalMonitorGpuRoute '/portal/monitor/idle': typeof PortalMonitorIdleRoute '/portal/monitor/network': typeof PortalMonitorNetworkRoute @@ -655,6 +686,7 @@ export interface FileRoutesByFullPath { '/admin/operation-logs': typeof AdminOperationLogsIndexRoute '/admin/statistics/': typeof AdminStatisticsIndexRoute '/admin/users/': typeof AdminUsersIndexRoute + '/portal/inference-services/': typeof PortalInferenceServicesIndexRoute '/portal/more/': typeof PortalMoreIndexRoute '/portal/overview/': typeof PortalOverviewIndexRoute '/portal/templates/': typeof PortalTemplatesIndexRoute @@ -716,6 +748,8 @@ export interface FileRoutesByTo { '/portal/account/member': typeof PortalAccountMemberRoute '/portal/account/statistics': typeof PortalAccountStatisticsRoute '/portal/files/$': typeof PortalFilesSplatRoute + '/portal/inference-services/$name': typeof PortalInferenceServicesNameRoute + '/portal/inference-services/new': typeof PortalInferenceServicesNewRoute '/portal/monitor/gpu': typeof PortalMonitorGpuRoute '/portal/monitor/idle': typeof PortalMonitorIdleRoute '/portal/monitor/network': typeof PortalMonitorNetworkRoute @@ -732,6 +766,7 @@ export interface FileRoutesByTo { '/admin/operation-logs': typeof AdminOperationLogsIndexRoute '/admin/statistics': typeof AdminStatisticsIndexRoute '/admin/users': typeof AdminUsersIndexRoute + '/portal/inference-services': typeof PortalInferenceServicesIndexRoute '/portal/more': typeof PortalMoreIndexRoute '/portal/overview': typeof PortalOverviewIndexRoute '/portal/templates': typeof PortalTemplatesIndexRoute @@ -783,6 +818,7 @@ export interface FileRoutesById { '/admin/statistics': typeof AdminStatisticsRouteRouteWithChildren '/admin/users': typeof AdminUsersRouteRouteWithChildren '/portal/account': typeof PortalAccountRouteRouteWithChildren + '/portal/inference-services': typeof PortalInferenceServicesRouteRouteWithChildren '/portal/more': typeof PortalMoreRouteRouteWithChildren '/portal/overview': typeof PortalOverviewRouteRouteWithChildren '/portal/templates': typeof PortalTemplatesRouteRouteWithChildren @@ -815,6 +851,8 @@ export interface FileRoutesById { '/portal/account/member': typeof PortalAccountMemberRoute '/portal/account/statistics': typeof PortalAccountStatisticsRoute '/portal/files/$': typeof PortalFilesSplatRoute + '/portal/inference-services/$name': typeof PortalInferenceServicesNameRoute + '/portal/inference-services/new': typeof PortalInferenceServicesNewRoute '/portal/monitor/gpu': typeof PortalMonitorGpuRoute '/portal/monitor/idle': typeof PortalMonitorIdleRoute '/portal/monitor/network': typeof PortalMonitorNetworkRoute @@ -831,6 +869,7 @@ export interface FileRoutesById { '/admin/operation-logs/': typeof AdminOperationLogsIndexRoute '/admin/statistics/': typeof AdminStatisticsIndexRoute '/admin/users/': typeof AdminUsersIndexRoute + '/portal/inference-services/': typeof PortalInferenceServicesIndexRoute '/portal/more/': typeof PortalMoreIndexRoute '/portal/overview/': typeof PortalOverviewIndexRoute '/portal/templates/': typeof PortalTemplatesIndexRoute @@ -883,6 +922,7 @@ export interface FileRouteTypes { | '/admin/statistics' | '/admin/users' | '/portal/account' + | '/portal/inference-services' | '/portal/more' | '/portal/overview' | '/portal/templates' @@ -915,6 +955,8 @@ export interface FileRouteTypes { | '/portal/account/member' | '/portal/account/statistics' | '/portal/files/$' + | '/portal/inference-services/$name' + | '/portal/inference-services/new' | '/portal/monitor/gpu' | '/portal/monitor/idle' | '/portal/monitor/network' @@ -931,6 +973,7 @@ export interface FileRouteTypes { | '/admin/operation-logs' | '/admin/statistics/' | '/admin/users/' + | '/portal/inference-services/' | '/portal/more/' | '/portal/overview/' | '/portal/templates/' @@ -992,6 +1035,8 @@ export interface FileRouteTypes { | '/portal/account/member' | '/portal/account/statistics' | '/portal/files/$' + | '/portal/inference-services/$name' + | '/portal/inference-services/new' | '/portal/monitor/gpu' | '/portal/monitor/idle' | '/portal/monitor/network' @@ -1008,6 +1053,7 @@ export interface FileRouteTypes { | '/admin/operation-logs' | '/admin/statistics' | '/admin/users' + | '/portal/inference-services' | '/portal/more' | '/portal/overview' | '/portal/templates' @@ -1058,6 +1104,7 @@ export interface FileRouteTypes { | '/admin/statistics' | '/admin/users' | '/portal/account' + | '/portal/inference-services' | '/portal/more' | '/portal/overview' | '/portal/templates' @@ -1090,6 +1137,8 @@ export interface FileRouteTypes { | '/portal/account/member' | '/portal/account/statistics' | '/portal/files/$' + | '/portal/inference-services/$name' + | '/portal/inference-services/new' | '/portal/monitor/gpu' | '/portal/monitor/idle' | '/portal/monitor/network' @@ -1106,6 +1155,7 @@ export interface FileRouteTypes { | '/admin/operation-logs/' | '/admin/statistics/' | '/admin/users/' + | '/portal/inference-services/' | '/portal/more/' | '/portal/overview/' | '/portal/templates/' @@ -1227,6 +1277,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PortalMoreRouteRouteImport parentRoute: typeof PortalRouteRoute } + '/portal/inference-services': { + id: '/portal/inference-services' + path: '/inference-services' + fullPath: '/portal/inference-services' + preLoaderRoute: typeof PortalInferenceServicesRouteRouteImport + parentRoute: typeof PortalRouteRoute + } '/portal/account': { id: '/portal/account' path: '/account' @@ -1304,6 +1361,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PortalMoreIndexRouteImport parentRoute: typeof PortalMoreRouteRoute } + '/portal/inference-services/': { + id: '/portal/inference-services/' + path: '/' + fullPath: '/portal/inference-services/' + preLoaderRoute: typeof PortalInferenceServicesIndexRouteImport + parentRoute: typeof PortalInferenceServicesRouteRoute + } '/admin/users/': { id: '/admin/users/' path: '/' @@ -1416,6 +1480,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PortalMonitorGpuRouteImport parentRoute: typeof PortalRouteRoute } + '/portal/inference-services/new': { + id: '/portal/inference-services/new' + path: '/new' + fullPath: '/portal/inference-services/new' + preLoaderRoute: typeof PortalInferenceServicesNewRouteImport + parentRoute: typeof PortalInferenceServicesRouteRoute + } + '/portal/inference-services/$name': { + id: '/portal/inference-services/$name' + path: '/$name' + fullPath: '/portal/inference-services/$name' + preLoaderRoute: typeof PortalInferenceServicesNameRouteImport + parentRoute: typeof PortalInferenceServicesRouteRoute + } '/portal/files/$': { id: '/portal/files/$' path: '/files/$' @@ -2032,6 +2110,24 @@ const PortalAccountRouteRouteChildren: PortalAccountRouteRouteChildren = { const PortalAccountRouteRouteWithChildren = PortalAccountRouteRoute._addFileChildren(PortalAccountRouteRouteChildren) +interface PortalInferenceServicesRouteRouteChildren { + PortalInferenceServicesNameRoute: typeof PortalInferenceServicesNameRoute + PortalInferenceServicesNewRoute: typeof PortalInferenceServicesNewRoute + PortalInferenceServicesIndexRoute: typeof PortalInferenceServicesIndexRoute +} + +const PortalInferenceServicesRouteRouteChildren: PortalInferenceServicesRouteRouteChildren = + { + PortalInferenceServicesNameRoute: PortalInferenceServicesNameRoute, + PortalInferenceServicesNewRoute: PortalInferenceServicesNewRoute, + PortalInferenceServicesIndexRoute: PortalInferenceServicesIndexRoute, + } + +const PortalInferenceServicesRouteRouteWithChildren = + PortalInferenceServicesRouteRoute._addFileChildren( + PortalInferenceServicesRouteRouteChildren, + ) + interface PortalMoreOrdersRouteRouteChildren { PortalMoreOrdersIdRoute: typeof PortalMoreOrdersIdRoute PortalMoreOrdersIndexRoute: typeof PortalMoreOrdersIndexRoute @@ -2213,6 +2309,7 @@ const PortalJobsNewRouteRouteWithChildren = interface PortalRouteRouteChildren { PortalAccountRouteRoute: typeof PortalAccountRouteRouteWithChildren + PortalInferenceServicesRouteRoute: typeof PortalInferenceServicesRouteRouteWithChildren PortalMoreRouteRoute: typeof PortalMoreRouteRouteWithChildren PortalOverviewRouteRoute: typeof PortalOverviewRouteRouteWithChildren PortalTemplatesRouteRoute: typeof PortalTemplatesRouteRouteWithChildren @@ -2235,6 +2332,8 @@ interface PortalRouteRouteChildren { const PortalRouteRouteChildren: PortalRouteRouteChildren = { PortalAccountRouteRoute: PortalAccountRouteRouteWithChildren, + PortalInferenceServicesRouteRoute: + PortalInferenceServicesRouteRouteWithChildren, PortalMoreRouteRoute: PortalMoreRouteRouteWithChildren, PortalOverviewRouteRoute: PortalOverviewRouteRouteWithChildren, PortalTemplatesRouteRoute: PortalTemplatesRouteRouteWithChildren, diff --git a/frontend/src/routes/admin/more/-components/kthena-inference-settings.tsx b/frontend/src/routes/admin/more/-components/kthena-inference-settings.tsx new file mode 100644 index 000000000..be25103de --- /dev/null +++ b/frontend/src/routes/admin/more/-components/kthena-inference-settings.tsx @@ -0,0 +1,79 @@ +import { CheckCircle2Icon, Loader2Icon, RocketIcon, UnplugIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' + +interface KthenaInferenceSettingsProps { + enabled: boolean + isPending: boolean + onToggle: (checked: boolean) => void +} + +export function KthenaInferenceSettings({ + enabled, + isPending, + onToggle, +}: KthenaInferenceSettingsProps) { + const { t } = useTranslation() + + return ( + <> + +
+ + + {t('systemConfig.kthenaInference.title', { defaultValue: '模型部署' })} + +
+ + {t('systemConfig.kthenaInference.description', { + defaultValue: '使用 Kthena 提供在线模型服务的部署与调用能力。', + })} + +
+ +
+
+ +

+ {t('systemConfig.kthenaInference.switchDescription', { + defaultValue: '开启后,用户可以创建、管理和调用在线模型服务。', + })} +

+
+
+ {isPending && } + +
+
+ + {!enabled && ( +
+ +

+ {t('systemConfig.kthenaInference.disabledWarning', { + defaultValue: '功能关闭后将隐藏入口,且直接访问模型部署页面也会被拒绝。', + })} +

+
+ )} + {enabled && ( +
+ +

+ {t('systemConfig.kthenaInference.activeNotice', { + defaultValue: '模型部署已开启,用户可以使用 Kthena 创建在线模型服务。', + })} +

+
+ )} +
+ + ) +} diff --git a/frontend/src/routes/admin/more/index.tsx b/frontend/src/routes/admin/more/index.tsx index ad85054d6..82a4dcfc7 100644 --- a/frontend/src/routes/admin/more/index.tsx +++ b/frontend/src/routes/admin/more/index.tsx @@ -14,6 +14,7 @@ import WarningAlert from '@/components/custom/warning-alert' import { apiAdminGetBillingStatus, apiAdminGetGpuAnalysisStatus, + apiAdminGetKthenaInferenceStatus, apiAdminGetLLMConfig, apiAdminGetModelDownloadLimitConfig, apiAdminGetPodBandwidthConfig, @@ -23,6 +24,7 @@ import { apiAdminResetLLMConfig, apiAdminSetBillingStatus, apiAdminSetGpuAnalysisStatus, + apiAdminSetKthenaInferenceStatus, apiAdminUpdateLLMConfig, apiAdminUpdateModelDownloadLimitConfig, apiAdminUpdatePodBandwidthConfig, @@ -37,6 +39,7 @@ import { showErrorToast } from '@/utils/toast' import { BasicSettings } from './-components/basic-settings' import { BillingSettings } from './-components/billing-settings' import { GpuAnalysis } from './-components/gpu-analysis' +import { KthenaInferenceSettings } from './-components/kthena-inference-settings' import { LlmFormSchema, LlmSettings, createLlmSettingsSchema } from './-components/llm-settings' import { ModelDownloadLimitSettings } from './-components/model-download-limit-settings' import { PodBandwidthSettings } from './-components/pod-bandwidth-settings' @@ -73,6 +76,11 @@ function RouteComponent() { queryFn: () => apiAdminGetGpuAnalysisStatus().then((res) => res.data), }) + const { data: kthenaInferenceStatusData } = useQuery({ + queryKey: ['admin', 'system-config', 'kthena-inference'], + queryFn: () => apiAdminGetKthenaInferenceStatus().then((res) => res.data), + }) + const { data: prequeueConfigData } = useQuery({ queryKey: ['admin', 'system-config', 'prequeue'], queryFn: () => apiAdminGetPrequeueConfig().then((res) => res.data), @@ -189,6 +197,28 @@ function RouteComponent() { onError: handleError, }) + const toggleKthenaInferenceMutation = useMutation({ + mutationFn: apiAdminSetKthenaInferenceStatus, + onSuccess: async (_data, enabled) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: ['admin', 'system-config', 'kthena-inference'], + }), + queryClient.invalidateQueries({ queryKey: ['system-config', 'kthena-inference'] }), + ]) + toast.success( + enabled + ? t('systemConfig.kthenaInference.enabledSuccess', { + defaultValue: '模型部署已开启', + }) + : t('systemConfig.kthenaInference.disabledSuccess', { + defaultValue: '模型部署已关闭', + }) + ) + }, + onError: showErrorToast, + }) + const updateBillingMutation = useMutation({ mutationFn: apiAdminSetBillingStatus, onSuccess: () => { @@ -264,6 +294,10 @@ function RouteComponent() { } } + const handleKthenaInferenceToggle = (checked: boolean) => { + toggleKthenaInferenceMutation.mutate(checked) + } + const buildPrequeuePayload = () => ({ backfillEnabled, queueQuotaEnabled, @@ -361,6 +395,14 @@ function RouteComponent() { /> + + + + , + loader: ({ params }) => ({ crumb: params.name }), +}) + +const getResourcePhaseVariant = ( + phase: string +): 'default' | 'secondary' | 'outline' | 'destructive' => { + if (phase === 'Ready' || phase === 'Active') return 'default' + if (phase === 'Failed') return 'destructive' + if (phase === 'Pending' || phase === 'Progressing') return 'secondary' + return 'outline' +} + +type ChatMessage = ChatCompletionReq['messages'][number] + +type ChatSession = { + id: string + title: string + messages: ChatMessage[] + createdAt: string + updatedAt: string +} + +const emptyChatMessages: ChatMessage[] = [] +const draftChatSessionKey = '__new__' +const getChatSessionStorageKey = (service: Pick) => + `kthena-chat-sessions:${service.namespace}:${service.name}` +const getLegacyChatSessionStorageKey = (serviceName: string) => + `kthena-chat-sessions:${serviceName}` +const getLegacyChatStorageKey = (serviceName: string) => `kthena-chat-history:${serviceName}` +const getChatMigrationStorageKey = ( + userID: number | undefined, + accountScope: string | undefined, + service: Pick +) => + `kthena-chat-server-migration:v1:${userID ?? 'anonymous'}:${accountScope ?? 'default'}:${service.namespace}:${service.name}` + +function KthenaServiceDetailPage() { + const { t } = useTranslation() + const navigate = Route.useNavigate() + const { name } = Route.useParams() + const queryClient = useQueryClient() + const [deleteOpen, setDeleteOpen] = useState(false) + const { data, isLoading } = useQuery({ + queryKey: ['kthena/inference-services', name], + queryFn: () => apiGetKthenaService(name).then((res) => res.data), + refetchInterval: REFETCH_INTERVAL, + }) + + const service = data + const { mutate: deleteService, isPending: isDeleting } = useMutation({ + mutationFn: apiDeleteKthenaService, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['kthena/inference-services'] }) + toast.success(t('kthena.delete.success')) + navigate({ to: '/portal/inference-services' }) + }, + onError: showErrorToast, + }) + + if (isLoading || !service) { + return ( +
+ + + + + + {t('kthena.loading')} + + +
+ ) + } + + const workerResources = getWorkerResources(service) + const primaryPod = service.runtimePods?.find((pod) => pod.ready) ?? service.runtimePods?.[0] + + return ( +
+ + + + + + +
+ + + + + {service.backendType || '-'} + + + + {service.servedModel || '-'} + + + + {Object.keys(workerResources).length ? ( + + ) : ( + - + )} + + + {service.userInfo?.username ? ( + + ) : ( + {service.owner || '-'} + )} + + + + +
+ + + + + + {t('kthena.detail.tabs.invoke')} + + + + {t('kthena.detail.tabs.overview')} + + + + {t('kthena.detail.tabs.resources')} + + + + {t('kthena.detail.tabs.usage')} + + + + {t('kthena.detail.tabs.diagnostics')} + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + +
+
+
+ +
+
+ + + + + {t('kthena.detail.tabs.resources')} + + + {service.resources?.length ? ( + service.resources.map((resource) => ( + + )) + ) : ( +
+ {t('kthena.detail.noResources')} +
+ )} +
+
+
+ + + + + + + + + {t('kthena.detail.diagnosticsTitle')} + + + {service.diagnostics?.length ? ( + <> + +
+ {service.diagnostics.map((diagnostic, index) => ( + + ))} +
+ + ) : ( +
+ {t('kthena.detail.noDiagnostics')} +
+ )} +
+
+
+ + + + +
+ + + + {t('kthena.delete.title')} + + {t('kthena.delete.description', { name: service.name })} + + + + + {t('kthena.actions.cancel')} + + deleteService(service.name)} + > + {t('kthena.actions.delete')} + + + + +
+ ) +} + +function KthenaDetailMeta({ + icon: Icon, + label, + children, +}: { + icon: typeof ActivityIcon + label: string + children: ReactNode +}) { + return ( +
+ + {label}: + {children} +
+ ) +} + +function InvokeWorkspace({ service }: { service: KthenaService }) { + const { t } = useTranslation() + const queryClient = useQueryClient() + const user = useAtomValue(atomUserInfo) + const accountContext = useAtomValue(atomUserContext) + const conversationQueryKey = useMemo( + () => + [ + 'kthena', + 'inference-services', + service.namespace, + service.name, + 'conversations', + user?.id, + accountContext?.space, + ] as const, + [accountContext?.space, service.name, service.namespace, user?.id] + ) + const [legacySessions] = useState(() => readStoredChatSessions(service)) + const legacyMigrationAttemptedRef = useRef(false) + const [activeSessionID, setActiveSessionID] = useState(undefined) + const [prompt, setPrompt] = useState('') + const [isSessionRailCollapsed, setIsSessionRailCollapsed] = useState(false) + const [responseBySession, setResponseBySession] = useState< + Record + >({}) + const [pendingBySession, setPendingBySession] = useState>({}) + const [pendingMessageBySession, setPendingMessageBySession] = useState< + Record + >({}) + const messagesViewportRef = useRef(null) + const promptInputRef = useRef(null) + const { + data: sessions = [], + isLoading: isSessionsLoading, + isSuccess: areSessionsLoaded, + } = useQuery({ + queryKey: [ + 'kthena', + 'inference-services', + service.namespace, + service.name, + 'conversations', + user?.id, + accountContext?.space, + ], + queryFn: () => + apiListKthenaConversations(service.name, { + includeMessages: true, + limit: 100, + messageLimit: 500, + }).then((res) => res.data), + enabled: Boolean(user?.id), + }) + + useEffect(() => { + if (activeSessionID === undefined) { + setActiveSessionID(sessions[0]?.sessionId ?? null) + return + } + if (activeSessionID && !sessions.some((session) => session.sessionId === activeSessionID)) { + setActiveSessionID(sessions[0]?.sessionId ?? null) + } + }, [activeSessionID, sessions]) + + useEffect(() => { + if (!user?.id || !areSessionsLoaded || legacySessions.length === 0) return + if (legacyMigrationAttemptedRef.current) return + const migrationKey = getChatMigrationStorageKey(user.id, accountContext?.space, service) + try { + if (window.localStorage.getItem(migrationKey)) return + } catch { + return + } + legacyMigrationAttemptedRef.current = true + void Promise.all( + legacySessions.slice(0, 100).map((session) => + apiCreateKthenaConversation(service.name, { + sessionId: session.id, + title: session.title, + messages: session.messages, + }) + ) + ) + .then(async () => { + try { + window.localStorage.setItem(migrationKey, new Date().toISOString()) + } catch { + // The server import is still complete when this browser cannot retain the marker. + } + await queryClient.invalidateQueries({ queryKey: conversationQueryKey }) + }) + .catch(() => { + // Preserve source data and leave a retry possible after the next page load. + legacyMigrationAttemptedRef.current = false + }) + }, [ + accountContext?.space, + areSessionsLoaded, + conversationQueryKey, + legacySessions, + queryClient, + service, + user?.id, + ]) + + const activeSession = activeSessionID + ? sessions.find((session) => session.sessionId === activeSessionID) + : undefined + const messages = activeSession?.messages ?? emptyChatMessages + const activeSessionKey = activeSession?.sessionId ?? draftChatSessionKey + const pendingMessage = pendingMessageBySession[activeSessionKey] + const displayedMessages = useMemo( + () => (pendingMessage ? [...messages, pendingMessage] : messages), + [messages, pendingMessage] + ) + const isActiveSessionPending = Boolean(pendingBySession[activeSessionKey]) + const rawResponse = responseBySession[activeSessionKey] + const pendingMessages = useMemo( + () => + prompt.trim() + ? [...displayedMessages, { role: 'user', content: prompt.trim() } satisfies ChatMessage] + : displayedMessages, + [displayedMessages, prompt] + ) + const curl = useMemo( + () => buildCurl(service, pendingMessages, t('kthena.detail.defaultPrompt')), + [pendingMessages, service, t] + ) + + useEffect(() => { + const viewport = messagesViewportRef.current + if (!viewport) return + viewport.scrollTo({ top: viewport.scrollHeight, behavior: 'smooth' }) + }, [activeSession?.sessionId, displayedMessages.length, isActiveSessionPending]) + + const focusPromptInput = () => { + window.requestAnimationFrame(() => promptInputRef.current?.focus()) + } + + const upsertConversation = (conversation: KthenaConversation) => { + queryClient.setQueryData(conversationQueryKey, (current) => { + const next = [ + conversation, + ...(current ?? []).filter((item) => item.sessionId !== conversation.sessionId), + ] + return next.sort( + (left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime() + ) + }) + } + + const startNewSession = () => { + setActiveSessionID(null) + setPrompt('') + focusPromptInput() + } + + const selectSession = (sessionID: string) => { + setActiveSessionID(sessionID) + setPrompt('') + focusPromptInput() + } + + const { mutate: clearSession, isPending: isClearingSession } = useMutation({ + mutationFn: (sessionID: string) => + apiUpdateKthenaConversation(service.name, sessionID, { title: '', messages: [] }).then( + (res) => res.data + ), + onSuccess: (conversation) => { + upsertConversation(conversation) + setResponseBySession((current) => { + const next = { ...current } + delete next[conversation.sessionId] + return next + }) + focusPromptInput() + }, + onError: showErrorToast, + }) + + const clearActiveSession = () => { + if (!activeSession) { + setPrompt('') + return + } + clearSession(activeSession.sessionId) + } + + const [deletingSessionID, setDeletingSessionID] = useState(null) + const { mutate: removeSession } = useMutation({ + mutationFn: (sessionID: string) => apiDeleteKthenaConversation(service.name, sessionID), + onMutate: (sessionID) => setDeletingSessionID(sessionID), + onSuccess: (_response, sessionID) => { + queryClient.setQueryData(conversationQueryKey, (current) => + (current ?? []).filter((session) => session.sessionId !== sessionID) + ) + if (activeSessionID === sessionID) { + setActiveSessionID(undefined) + setPrompt('') + } + setResponseBySession((current) => { + const next = { ...current } + delete next[sessionID] + return next + }) + }, + onError: showErrorToast, + onSettled: () => setDeletingSessionID(null), + }) + + const { mutate: sendMessage } = useMutation({ + mutationFn: ({ + sessionID, + content, + clientTurnID, + }: { + sessionID?: string + content: string + clientTurnID: string + }) => + apiCreateKthenaConversationTurn(service.name, { + sessionId: sessionID, + content, + temperature: 0.2, + clientTurnId: clientTurnID, + }).then((res) => res.data), + onMutate: ({ sessionID, content }) => { + const sessionKey = sessionID ?? draftChatSessionKey + setPendingBySession((current) => ({ ...current, [sessionKey]: true })) + setPendingMessageBySession((current) => ({ + ...current, + [sessionKey]: { role: 'user', content }, + })) + return { sessionKey } + }, + onSuccess: (response) => { + upsertConversation(response.conversation) + setActiveSessionID(response.conversation.sessionId) + setResponseBySession((current) => ({ + ...current, + [response.conversation.sessionId]: response.completion ?? undefined, + })) + }, + onError: (error, variables, context) => { + if (context?.sessionKey === activeSessionKey) { + setPrompt(variables.content) + } + showErrorToast(error) + }, + onSettled: (_data, _error, _variables, context) => { + const sessionKey = context?.sessionKey + if (!sessionKey) return + setPendingBySession((current) => { + const next = { ...current } + delete next[sessionKey] + return next + }) + setPendingMessageBySession((current) => { + const next = { ...current } + delete next[sessionKey] + return next + }) + }, + }) + + const submitMessage = () => { + const content = prompt.trim() + if (!content || isActiveSessionPending || isSessionsLoading) return + setPrompt('') + setResponseBySession((current) => { + const next = { ...current } + delete next[activeSessionKey] + return next + }) + sendMessage({ + sessionID: activeSession?.sessionId, + content, + clientTurnID: createChatID(), + }) + } + + return ( +
+ +
+ + +
+
+
+
{t('kthena.detail.onlineTest')}
+
+ {activeSession?.title || t('kthena.chat.untitledSession')} +
+
+
+ + {service.access?.modelName || service.name} + + +
+
+ +
+ {sessions.map((session) => ( +
+ + +
+ ))} +
+ +
+ {displayedMessages.length ? ( +
+ {displayedMessages.map((message, index) => ( + + ))} + {isActiveSessionPending && ( + + )} +
+ ) : ( +
+
+ +
+
+ {t('kthena.chat.emptyTitle')} +
+

{t('kthena.detail.responsePlaceholder')}

+
+ )} +
+ +
+
+