dstack 0.19.18__py3-none-any.whl → 0.19.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dstack might be problematic. Click here for more details.

Files changed (74) hide show
  1. dstack/_internal/cli/services/configurators/fleet.py +99 -1
  2. dstack/_internal/cli/services/profile.py +1 -1
  3. dstack/_internal/core/backends/cloudrift/api_client.py +13 -1
  4. dstack/_internal/core/backends/oci/resources.py +5 -5
  5. dstack/_internal/core/compatibility/runs.py +12 -1
  6. dstack/_internal/core/compatibility/volumes.py +2 -0
  7. dstack/_internal/core/models/common.py +38 -2
  8. dstack/_internal/core/models/configurations.py +9 -1
  9. dstack/_internal/core/models/fleets.py +2 -1
  10. dstack/_internal/core/models/profiles.py +8 -5
  11. dstack/_internal/core/models/resources.py +15 -8
  12. dstack/_internal/core/models/runs.py +41 -138
  13. dstack/_internal/core/models/volumes.py +14 -0
  14. dstack/_internal/core/services/diff.py +30 -10
  15. dstack/_internal/core/services/ssh/attach.py +2 -0
  16. dstack/_internal/server/app.py +17 -9
  17. dstack/_internal/server/background/__init__.py +5 -3
  18. dstack/_internal/server/background/tasks/process_gateways.py +46 -28
  19. dstack/_internal/server/background/tasks/process_idle_volumes.py +139 -0
  20. dstack/_internal/server/background/tasks/process_submitted_jobs.py +2 -0
  21. dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +6 -6
  22. dstack/_internal/server/migrations/versions/d5863798bf41_add_volumemodel_last_job_processed_at.py +40 -0
  23. dstack/_internal/server/models.py +1 -0
  24. dstack/_internal/server/routers/backends.py +23 -16
  25. dstack/_internal/server/routers/files.py +7 -6
  26. dstack/_internal/server/routers/fleets.py +47 -36
  27. dstack/_internal/server/routers/gateways.py +27 -18
  28. dstack/_internal/server/routers/instances.py +18 -13
  29. dstack/_internal/server/routers/logs.py +7 -3
  30. dstack/_internal/server/routers/metrics.py +14 -8
  31. dstack/_internal/server/routers/projects.py +33 -22
  32. dstack/_internal/server/routers/repos.py +7 -6
  33. dstack/_internal/server/routers/runs.py +49 -28
  34. dstack/_internal/server/routers/secrets.py +20 -15
  35. dstack/_internal/server/routers/server.py +7 -4
  36. dstack/_internal/server/routers/users.py +22 -19
  37. dstack/_internal/server/routers/volumes.py +34 -25
  38. dstack/_internal/server/schemas/logs.py +3 -11
  39. dstack/_internal/server/schemas/runs.py +17 -5
  40. dstack/_internal/server/services/fleets.py +354 -72
  41. dstack/_internal/server/services/gateways/__init__.py +13 -4
  42. dstack/_internal/server/services/gateways/client.py +5 -3
  43. dstack/_internal/server/services/instances.py +8 -0
  44. dstack/_internal/server/services/jobs/__init__.py +45 -0
  45. dstack/_internal/server/services/jobs/configurators/base.py +7 -0
  46. dstack/_internal/server/services/locking.py +3 -1
  47. dstack/_internal/server/services/logging.py +4 -2
  48. dstack/_internal/server/services/logs/__init__.py +15 -2
  49. dstack/_internal/server/services/logs/aws.py +47 -7
  50. dstack/_internal/server/services/logs/filelog.py +148 -32
  51. dstack/_internal/server/services/logs/gcp.py +3 -5
  52. dstack/_internal/server/services/prometheus/custom_metrics.py +20 -0
  53. dstack/_internal/server/services/proxy/repo.py +4 -1
  54. dstack/_internal/server/services/runs.py +115 -32
  55. dstack/_internal/server/services/services/__init__.py +2 -1
  56. dstack/_internal/server/services/users.py +3 -1
  57. dstack/_internal/server/services/volumes.py +13 -0
  58. dstack/_internal/server/settings.py +7 -2
  59. dstack/_internal/server/statics/index.html +1 -1
  60. dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js → main-39a767528976f8078166.js} +11 -30
  61. dstack/_internal/server/statics/{main-d1ac2e8c38ed5f08a114.js.map → main-39a767528976f8078166.js.map} +1 -1
  62. dstack/_internal/server/statics/{main-d58fc0460cb0eae7cb5c.css → main-8f9ee218d3eb45989682.css} +2 -2
  63. dstack/_internal/server/testing/common.py +41 -5
  64. dstack/_internal/server/utils/routers.py +31 -8
  65. dstack/_internal/utils/common.py +10 -21
  66. dstack/_internal/utils/json_utils.py +54 -0
  67. dstack/api/_public/runs.py +13 -2
  68. dstack/api/server/_runs.py +12 -2
  69. dstack/version.py +1 -1
  70. {dstack-0.19.18.dist-info → dstack-0.19.20.dist-info}/METADATA +7 -5
  71. {dstack-0.19.18.dist-info → dstack-0.19.20.dist-info}/RECORD +74 -71
  72. {dstack-0.19.18.dist-info → dstack-0.19.20.dist-info}/WHEEL +0 -0
  73. {dstack-0.19.18.dist-info → dstack-0.19.20.dist-info}/entry_points.txt +0 -0
  74. {dstack-0.19.18.dist-info → dstack-0.19.20.dist-info}/licenses/LICENSE.md +0 -0
@@ -27,7 +27,10 @@ from dstack._internal.server.services.config import (
27
27
  get_backend_config_yaml,
28
28
  update_backend_config_yaml,
29
29
  )
30
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
30
+ from dstack._internal.server.utils.routers import (
31
+ CustomORJSONResponse,
32
+ get_base_api_additional_responses,
33
+ )
31
34
 
32
35
  root_router = APIRouter(
33
36
  prefix="/api/backends",
@@ -41,35 +44,37 @@ project_router = APIRouter(
41
44
  )
42
45
 
43
46
 
44
- @root_router.post("/list_types")
45
- async def list_backend_types() -> List[BackendType]:
46
- return dstack._internal.core.backends.configurators.list_available_backend_types()
47
+ @root_router.post("/list_types", response_model=List[BackendType])
48
+ async def list_backend_types():
49
+ return CustomORJSONResponse(
50
+ dstack._internal.core.backends.configurators.list_available_backend_types()
51
+ )
47
52
 
48
53
 
49
- @project_router.post("/create")
54
+ @project_router.post("/create", response_model=AnyBackendConfigWithCreds)
50
55
  async def create_backend(
51
56
  body: AnyBackendConfigWithCreds,
52
57
  session: AsyncSession = Depends(get_session),
53
58
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
54
- ) -> AnyBackendConfigWithCreds:
59
+ ):
55
60
  _, project = user_project
56
61
  config = await backends.create_backend(session=session, project=project, config=body)
57
62
  if settings.SERVER_CONFIG_ENABLED:
58
63
  await ServerConfigManager().sync_config(session=session)
59
- return config
64
+ return CustomORJSONResponse(config)
60
65
 
61
66
 
62
- @project_router.post("/update")
67
+ @project_router.post("/update", response_model=AnyBackendConfigWithCreds)
63
68
  async def update_backend(
64
69
  body: AnyBackendConfigWithCreds,
65
70
  session: AsyncSession = Depends(get_session),
66
71
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
67
- ) -> AnyBackendConfigWithCreds:
72
+ ):
68
73
  _, project = user_project
69
74
  config = await backends.update_backend(session=session, project=project, config=body)
70
75
  if settings.SERVER_CONFIG_ENABLED:
71
76
  await ServerConfigManager().sync_config(session=session)
72
- return config
77
+ return CustomORJSONResponse(config)
73
78
 
74
79
 
75
80
  @project_router.post("/delete")
@@ -86,16 +91,16 @@ async def delete_backends(
86
91
  await ServerConfigManager().sync_config(session=session)
87
92
 
88
93
 
89
- @project_router.post("/{backend_name}/config_info")
94
+ @project_router.post("/{backend_name}/config_info", response_model=AnyBackendConfigWithCreds)
90
95
  async def get_backend_config_info(
91
96
  backend_name: BackendType,
92
97
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
93
- ) -> AnyBackendConfigWithCreds:
98
+ ):
94
99
  _, project = user_project
95
100
  config = await backends.get_backend_config(project=project, backend_type=backend_name)
96
101
  if config is None:
97
102
  raise ResourceNotExistsError()
98
- return config
103
+ return CustomORJSONResponse(config)
99
104
 
100
105
 
101
106
  @project_router.post("/create_yaml")
@@ -126,10 +131,12 @@ async def update_backend_yaml(
126
131
  )
127
132
 
128
133
 
129
- @project_router.post("/{backend_name}/get_yaml")
134
+ @project_router.post("/{backend_name}/get_yaml", response_model=BackendInfoYAML)
130
135
  async def get_backend_yaml(
131
136
  backend_name: BackendType,
132
137
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
133
- ) -> BackendInfoYAML:
138
+ ):
134
139
  _, project = user_project
135
- return await get_backend_config_yaml(project=project, backend_type=backend_name)
140
+ return CustomORJSONResponse(
141
+ await get_backend_config_yaml(project=project, backend_type=backend_name)
142
+ )
@@ -12,6 +12,7 @@ from dstack._internal.server.security.permissions import Authenticated
12
12
  from dstack._internal.server.services import files
13
13
  from dstack._internal.server.settings import SERVER_CODE_UPLOAD_LIMIT
14
14
  from dstack._internal.server.utils.routers import (
15
+ CustomORJSONResponse,
15
16
  get_base_api_additional_responses,
16
17
  get_request_size,
17
18
  )
@@ -24,12 +25,12 @@ router = APIRouter(
24
25
  )
25
26
 
26
27
 
27
- @router.post("/get_archive_by_hash")
28
+ @router.post("/get_archive_by_hash", response_model=FileArchive)
28
29
  async def get_archive_by_hash(
29
30
  body: GetFileArchiveByHashRequest,
30
31
  session: Annotated[AsyncSession, Depends(get_session)],
31
32
  user: Annotated[UserModel, Depends(Authenticated())],
32
- ) -> FileArchive:
33
+ ):
33
34
  archive = await files.get_archive_by_hash(
34
35
  session=session,
35
36
  user=user,
@@ -37,16 +38,16 @@ async def get_archive_by_hash(
37
38
  )
38
39
  if archive is None:
39
40
  raise ResourceNotExistsError()
40
- return archive
41
+ return CustomORJSONResponse(archive)
41
42
 
42
43
 
43
- @router.post("/upload_archive")
44
+ @router.post("/upload_archive", response_model=FileArchive)
44
45
  async def upload_archive(
45
46
  request: Request,
46
47
  file: UploadFile,
47
48
  session: Annotated[AsyncSession, Depends(get_session)],
48
49
  user: Annotated[UserModel, Depends(Authenticated())],
49
- ) -> FileArchive:
50
+ ):
50
51
  request_size = get_request_size(request)
51
52
  if SERVER_CODE_UPLOAD_LIMIT > 0 and request_size > SERVER_CODE_UPLOAD_LIMIT:
52
53
  diff_size_fmt = sizeof_fmt(request_size)
@@ -64,4 +65,4 @@ async def upload_archive(
64
65
  user=user,
65
66
  file=file,
66
67
  )
67
- return archive
68
+ return CustomORJSONResponse(archive)
@@ -18,7 +18,10 @@ from dstack._internal.server.schemas.fleets import (
18
18
  ListFleetsRequest,
19
19
  )
20
20
  from dstack._internal.server.security.permissions import Authenticated, ProjectMember
21
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
21
+ from dstack._internal.server.utils.routers import (
22
+ CustomORJSONResponse,
23
+ get_base_api_additional_responses,
24
+ )
22
25
 
23
26
  root_router = APIRouter(
24
27
  prefix="/api/fleets",
@@ -32,12 +35,12 @@ project_router = APIRouter(
32
35
  )
33
36
 
34
37
 
35
- @root_router.post("/list")
38
+ @root_router.post("/list", response_model=List[Fleet])
36
39
  async def list_fleets(
37
40
  body: ListFleetsRequest,
38
41
  session: AsyncSession = Depends(get_session),
39
42
  user: UserModel = Depends(Authenticated()),
40
- ) -> List[Fleet]:
43
+ ):
41
44
  """
42
45
  Returns all fleets and instances within them visible to user sorted by descending `created_at`.
43
46
  `project_name` and `only_active` can be specified as filters.
@@ -45,36 +48,40 @@ async def list_fleets(
45
48
  The results are paginated. To get the next page, pass `created_at` and `id` of
46
49
  the last fleet from the previous page as `prev_created_at` and `prev_id`.
47
50
  """
48
- return await fleets_services.list_fleets(
49
- session=session,
50
- user=user,
51
- project_name=body.project_name,
52
- only_active=body.only_active,
53
- prev_created_at=body.prev_created_at,
54
- prev_id=body.prev_id,
55
- limit=body.limit,
56
- ascending=body.ascending,
51
+ return CustomORJSONResponse(
52
+ await fleets_services.list_fleets(
53
+ session=session,
54
+ user=user,
55
+ project_name=body.project_name,
56
+ only_active=body.only_active,
57
+ prev_created_at=body.prev_created_at,
58
+ prev_id=body.prev_id,
59
+ limit=body.limit,
60
+ ascending=body.ascending,
61
+ )
57
62
  )
58
63
 
59
64
 
60
- @project_router.post("/list")
65
+ @project_router.post("/list", response_model=List[Fleet])
61
66
  async def list_project_fleets(
62
67
  session: AsyncSession = Depends(get_session),
63
68
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
64
- ) -> List[Fleet]:
69
+ ):
65
70
  """
66
71
  Returns all fleets in the project.
67
72
  """
68
73
  _, project = user_project
69
- return await fleets_services.list_project_fleets(session=session, project=project)
74
+ return CustomORJSONResponse(
75
+ await fleets_services.list_project_fleets(session=session, project=project)
76
+ )
70
77
 
71
78
 
72
- @project_router.post("/get")
79
+ @project_router.post("/get", response_model=Fleet)
73
80
  async def get_fleet(
74
81
  body: GetFleetRequest,
75
82
  session: AsyncSession = Depends(get_session),
76
83
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
77
- ) -> Fleet:
84
+ ):
78
85
  """
79
86
  Returns a fleet given `name` or `id`.
80
87
  If given `name`, does not return deleted fleets.
@@ -86,15 +93,15 @@ async def get_fleet(
86
93
  )
87
94
  if fleet is None:
88
95
  raise ResourceNotExistsError()
89
- return fleet
96
+ return CustomORJSONResponse(fleet)
90
97
 
91
98
 
92
- @project_router.post("/get_plan")
99
+ @project_router.post("/get_plan", response_model=FleetPlan)
93
100
  async def get_plan(
94
101
  body: GetFleetPlanRequest,
95
102
  session: AsyncSession = Depends(get_session),
96
103
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
97
- ) -> FleetPlan:
104
+ ):
98
105
  """
99
106
  Returns a fleet plan for the given fleet configuration.
100
107
  """
@@ -105,45 +112,49 @@ async def get_plan(
105
112
  user=user,
106
113
  spec=body.spec,
107
114
  )
108
- return plan
115
+ return CustomORJSONResponse(plan)
109
116
 
110
117
 
111
- @project_router.post("/apply")
118
+ @project_router.post("/apply", response_model=Fleet)
112
119
  async def apply_plan(
113
120
  body: ApplyFleetPlanRequest,
114
121
  session: AsyncSession = Depends(get_session),
115
122
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
116
- ) -> Fleet:
123
+ ):
117
124
  """
118
125
  Creates a new fleet or updates an existing fleet.
119
126
  Errors if the expected current resource from the plan does not match the current resource.
120
127
  Use `force: true` to apply even if the current resource does not match.
121
128
  """
122
129
  user, project = user_project
123
- return await fleets_services.apply_plan(
124
- session=session,
125
- user=user,
126
- project=project,
127
- plan=body.plan,
128
- force=body.force,
130
+ return CustomORJSONResponse(
131
+ await fleets_services.apply_plan(
132
+ session=session,
133
+ user=user,
134
+ project=project,
135
+ plan=body.plan,
136
+ force=body.force,
137
+ )
129
138
  )
130
139
 
131
140
 
132
- @project_router.post("/create")
141
+ @project_router.post("/create", response_model=Fleet)
133
142
  async def create_fleet(
134
143
  body: CreateFleetRequest,
135
144
  session: AsyncSession = Depends(get_session),
136
145
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
137
- ) -> Fleet:
146
+ ):
138
147
  """
139
148
  Creates a fleet given a fleet configuration.
140
149
  """
141
150
  user, project = user_project
142
- return await fleets_services.create_fleet(
143
- session=session,
144
- project=project,
145
- user=user,
146
- spec=body.spec,
151
+ return CustomORJSONResponse(
152
+ await fleets_services.create_fleet(
153
+ session=session,
154
+ project=project,
155
+ user=user,
156
+ spec=body.spec,
157
+ )
147
158
  )
148
159
 
149
160
 
@@ -13,7 +13,10 @@ from dstack._internal.server.security.permissions import (
13
13
  ProjectAdmin,
14
14
  ProjectMemberOrPublicAccess,
15
15
  )
16
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
16
+ from dstack._internal.server.utils.routers import (
17
+ CustomORJSONResponse,
18
+ get_base_api_additional_responses,
19
+ )
17
20
 
18
21
  router = APIRouter(
19
22
  prefix="/api/project/{project_name}/gateways",
@@ -22,40 +25,44 @@ router = APIRouter(
22
25
  )
23
26
 
24
27
 
25
- @router.post("/list")
28
+ @router.post("/list", response_model=List[models.Gateway])
26
29
  async def list_gateways(
27
30
  session: AsyncSession = Depends(get_session),
28
31
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMemberOrPublicAccess()),
29
- ) -> List[models.Gateway]:
32
+ ):
30
33
  _, project = user_project
31
- return await gateways.list_project_gateways(session=session, project=project)
34
+ return CustomORJSONResponse(
35
+ await gateways.list_project_gateways(session=session, project=project)
36
+ )
32
37
 
33
38
 
34
- @router.post("/get")
39
+ @router.post("/get", response_model=models.Gateway)
35
40
  async def get_gateway(
36
41
  body: schemas.GetGatewayRequest,
37
42
  session: AsyncSession = Depends(get_session),
38
43
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMemberOrPublicAccess()),
39
- ) -> models.Gateway:
44
+ ):
40
45
  _, project = user_project
41
46
  gateway = await gateways.get_gateway_by_name(session=session, project=project, name=body.name)
42
47
  if gateway is None:
43
48
  raise ResourceNotExistsError()
44
- return gateway
49
+ return CustomORJSONResponse(gateway)
45
50
 
46
51
 
47
- @router.post("/create")
52
+ @router.post("/create", response_model=models.Gateway)
48
53
  async def create_gateway(
49
54
  body: schemas.CreateGatewayRequest,
50
55
  session: AsyncSession = Depends(get_session),
51
56
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
52
- ) -> models.Gateway:
57
+ ):
53
58
  user, project = user_project
54
- return await gateways.create_gateway(
55
- session=session,
56
- user=user,
57
- project=project,
58
- configuration=body.configuration,
59
+ return CustomORJSONResponse(
60
+ await gateways.create_gateway(
61
+ session=session,
62
+ user=user,
63
+ project=project,
64
+ configuration=body.configuration,
65
+ )
59
66
  )
60
67
 
61
68
 
@@ -83,13 +90,15 @@ async def set_default_gateway(
83
90
  await gateways.set_default_gateway(session=session, project=project, name=body.name)
84
91
 
85
92
 
86
- @router.post("/set_wildcard_domain")
93
+ @router.post("/set_wildcard_domain", response_model=models.Gateway)
87
94
  async def set_gateway_wildcard_domain(
88
95
  body: schemas.SetWildcardDomainRequest,
89
96
  session: AsyncSession = Depends(get_session),
90
97
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectAdmin()),
91
- ) -> models.Gateway:
98
+ ):
92
99
  _, project = user_project
93
- return await gateways.set_gateway_wildcard_domain(
94
- session=session, project=project, name=body.name, wildcard_domain=body.wildcard_domain
100
+ return CustomORJSONResponse(
101
+ await gateways.set_gateway_wildcard_domain(
102
+ session=session, project=project, name=body.name, wildcard_domain=body.wildcard_domain
103
+ )
95
104
  )
@@ -9,7 +9,10 @@ from dstack._internal.server.db import get_session
9
9
  from dstack._internal.server.models import UserModel
10
10
  from dstack._internal.server.schemas.instances import ListInstancesRequest
11
11
  from dstack._internal.server.security.permissions import Authenticated
12
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
12
+ from dstack._internal.server.utils.routers import (
13
+ CustomORJSONResponse,
14
+ get_base_api_additional_responses,
15
+ )
13
16
 
14
17
  root_router = APIRouter(
15
18
  prefix="/api/instances",
@@ -18,12 +21,12 @@ root_router = APIRouter(
18
21
  )
19
22
 
20
23
 
21
- @root_router.post("/list")
24
+ @root_router.post("/list", response_model=List[Instance])
22
25
  async def list_instances(
23
26
  body: ListInstancesRequest,
24
27
  session: AsyncSession = Depends(get_session),
25
28
  user: UserModel = Depends(Authenticated()),
26
- ) -> List[Instance]:
29
+ ):
27
30
  """
28
31
  Returns all instances visible to user sorted by descending `created_at`.
29
32
  `project_names` and `fleet_ids` can be specified as filters.
@@ -31,14 +34,16 @@ async def list_instances(
31
34
  The results are paginated. To get the next page, pass `created_at` and `id` of
32
35
  the last instance from the previous page as `prev_created_at` and `prev_id`.
33
36
  """
34
- return await instances.list_user_instances(
35
- session=session,
36
- user=user,
37
- project_names=body.project_names,
38
- fleet_ids=body.fleet_ids,
39
- only_active=body.only_active,
40
- prev_created_at=body.prev_created_at,
41
- prev_id=body.prev_id,
42
- limit=body.limit,
43
- ascending=body.ascending,
37
+ return CustomORJSONResponse(
38
+ await instances.list_user_instances(
39
+ session=session,
40
+ user=user,
41
+ project_names=body.project_names,
42
+ fleet_ids=body.fleet_ids,
43
+ only_active=body.only_active,
44
+ prev_created_at=body.prev_created_at,
45
+ prev_id=body.prev_id,
46
+ limit=body.limit,
47
+ ascending=body.ascending,
48
+ )
44
49
  )
@@ -7,7 +7,10 @@ from dstack._internal.server.models import ProjectModel, UserModel
7
7
  from dstack._internal.server.schemas.logs import PollLogsRequest
8
8
  from dstack._internal.server.security.permissions import ProjectMember
9
9
  from dstack._internal.server.services import logs
10
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
10
+ from dstack._internal.server.utils.routers import (
11
+ CustomORJSONResponse,
12
+ get_base_api_additional_responses,
13
+ )
11
14
 
12
15
  router = APIRouter(
13
16
  prefix="/api/project/{project_name}/logs",
@@ -18,13 +21,14 @@ router = APIRouter(
18
21
 
19
22
  @router.post(
20
23
  "/poll",
24
+ response_model=JobSubmissionLogs,
21
25
  )
22
26
  async def poll_logs(
23
27
  body: PollLogsRequest,
24
28
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
25
- ) -> JobSubmissionLogs:
29
+ ):
26
30
  _, project = user_project
27
31
  # The runner guarantees logs have different timestamps if throughput < 1k logs / sec.
28
32
  # Otherwise, some logs with duplicated timestamps may be filtered out.
29
33
  # This limitation is imposed by cloud log services that support up to millisecond timestamp resolution.
30
- return await logs.poll_logs_async(project=project, request=body)
34
+ return CustomORJSONResponse(await logs.poll_logs_async(project=project, request=body))
@@ -11,7 +11,10 @@ from dstack._internal.server.models import ProjectModel, UserModel
11
11
  from dstack._internal.server.security.permissions import ProjectMember
12
12
  from dstack._internal.server.services import metrics
13
13
  from dstack._internal.server.services.jobs import get_run_job_model
14
- from dstack._internal.server.utils.routers import get_base_api_additional_responses
14
+ from dstack._internal.server.utils.routers import (
15
+ CustomORJSONResponse,
16
+ get_base_api_additional_responses,
17
+ )
15
18
 
16
19
  router = APIRouter(
17
20
  prefix="/api/project/{project_name}/metrics",
@@ -22,6 +25,7 @@ router = APIRouter(
22
25
 
23
26
  @router.get(
24
27
  "/job/{run_name}",
28
+ response_model=JobMetrics,
25
29
  )
26
30
  async def get_job_metrics(
27
31
  run_name: str,
@@ -32,7 +36,7 @@ async def get_job_metrics(
32
36
  before: Optional[datetime] = None,
33
37
  session: AsyncSession = Depends(get_session),
34
38
  user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
35
- ) -> JobMetrics:
39
+ ):
36
40
  """
37
41
  Returns job-level metrics such as hardware utilization
38
42
  given `run_name`, `replica_num`, and `job_num`.
@@ -63,10 +67,12 @@ async def get_job_metrics(
63
67
  if job_model is None:
64
68
  raise ResourceNotExistsError("Found no job with given parameters")
65
69
 
66
- return await metrics.get_job_metrics(
67
- session=session,
68
- job_model=job_model,
69
- limit=limit,
70
- after=after,
71
- before=before,
70
+ return CustomORJSONResponse(
71
+ await metrics.get_job_metrics(
72
+ session=session,
73
+ job_model=job_model,
74
+ limit=limit,
75
+ after=after,
76
+ before=before,
77
+ )
72
78
  )