Skip to content

Commit 3fa980c

Browse files
jopemachineclaude
andcommitted
feat(BA-6893): add kernel scheduling-history SDK v2 client and CLI commands
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a7164c commit 3fa980c

5 files changed

Lines changed: 282 additions & 2 deletions

File tree

.claude/skills/bai-cli/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Check options with `--help`.
5555
- **service-catalog**: user(empty group) · admin(search)
5656
- **runtime-variant**: user(get, search) · admin(get, search, create, update, delete, bulk-delete)
5757
- **runtime-variant-preset**: user(get, search) · admin(get, search, create, update, delete)
58-
- **scheduling-history**: sub session / deployment / route — each (search, search-scoped)
58+
- **scheduling-history**: sub session / kernel / deployment / route — each (search, search-scoped)
5959
- **scheduling-handler**: admin(list)
6060

6161
### Storage

changes/12866.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add kernel scheduling-history queries to REST v2, the SDK, and the CLI (`bai scheduling-history kernel search / search-scoped`).

src/ai/backend/client/cli/v2/scheduling_history/commands.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""CLI commands for scheduling history management.
22
3-
Commands are organized into sub-groups: ``session``, ``deployment``,
3+
Commands are organized into sub-groups: ``session``, ``kernel``, ``deployment``,
44
and ``route``.
55
"""
66

@@ -9,6 +9,7 @@
99
import click
1010

1111
from .deployment import deployment
12+
from .kernel import kernel
1213
from .route import route
1314
from .session import session
1415

@@ -20,5 +21,6 @@ def scheduling_history() -> None:
2021

2122
# Register sub-groups
2223
scheduling_history.add_command(session)
24+
scheduling_history.add_command(kernel)
2325
scheduling_history.add_command(deployment)
2426
scheduling_history.add_command(route)
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
"""CLI commands for kernel scheduling history."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from typing import TYPE_CHECKING
7+
from uuid import UUID
8+
9+
import click
10+
11+
from ai.backend.client.cli.v2.helpers import (
12+
create_v2_registry,
13+
load_v2_config,
14+
parse_order_options,
15+
print_result,
16+
)
17+
18+
if TYPE_CHECKING:
19+
from ai.backend.common.dto.manager.v2.scheduling_history.request import KernelHistoryFilter
20+
21+
# Shared result choices for scheduling history filters
22+
_RESULT_CHOICES = click.Choice(
23+
["SUCCESS", "FAILURE", "STALE", "NEED_RETRY", "EXPIRED", "GIVE_UP", "SKIPPED"],
24+
case_sensitive=False,
25+
)
26+
27+
28+
def _build_kernel_history_filter(
29+
*,
30+
phase: str | None,
31+
from_status: tuple[str, ...],
32+
to_status: tuple[str, ...],
33+
result: str | None,
34+
error_code: str | None,
35+
message: str | None,
36+
kernel_id: str | None = None,
37+
session_id: str | None = None,
38+
) -> KernelHistoryFilter | None:
39+
"""Build a KernelHistoryFilter from explicit CLI options.
40+
41+
``kernel_id`` and ``session_id`` are omitted by callers whose scope already
42+
narrows by that id. Returns None if no filter options were provided.
43+
"""
44+
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
45+
from ai.backend.common.dto.manager.v2.scheduling_history.request import (
46+
KernelHistoryFilter,
47+
SchedulingResultFilter,
48+
)
49+
from ai.backend.common.dto.manager.v2.scheduling_history.types import SchedulingResultType
50+
51+
has_any = any(
52+
opt is not None for opt in (kernel_id, session_id, phase, result, error_code, message)
53+
)
54+
if not has_any and not from_status and not to_status:
55+
return None
56+
57+
return KernelHistoryFilter(
58+
kernel_id=UUIDFilter(equals=UUID(kernel_id)) if kernel_id is not None else None,
59+
session_id=UUIDFilter(equals=UUID(session_id)) if session_id is not None else None,
60+
phase=StringFilter(contains=phase) if phase is not None else None,
61+
from_status=list(from_status) if from_status else None,
62+
to_status=list(to_status) if to_status else None,
63+
result=(
64+
SchedulingResultFilter(equals=SchedulingResultType(result))
65+
if result is not None
66+
else None
67+
),
68+
error_code=StringFilter(contains=error_code) if error_code is not None else None,
69+
message=StringFilter(contains=message) if message is not None else None,
70+
)
71+
72+
73+
@click.group()
74+
def kernel() -> None:
75+
"""Kernel scheduling history commands."""
76+
77+
78+
@kernel.command()
79+
@click.option("--limit", type=int, default=None, help="Maximum items to return.")
80+
@click.option("--offset", type=int, default=None, help="Number of items to skip.")
81+
@click.option("--kernel-id", type=str, default=None, help="Filter by kernel ID (UUID).")
82+
@click.option("--session-id", type=str, default=None, help="Filter by session ID (UUID).")
83+
@click.option("--phase", type=str, default=None, help="Filter by scheduling phase (contains).")
84+
@click.option(
85+
"--from-status",
86+
type=str,
87+
multiple=True,
88+
help="Filter by from_status values (repeatable).",
89+
)
90+
@click.option(
91+
"--to-status",
92+
type=str,
93+
multiple=True,
94+
help="Filter by to_status values (repeatable).",
95+
)
96+
@click.option("--result", type=_RESULT_CHOICES, default=None, help="Filter by scheduling result.")
97+
@click.option("--error-code", type=str, default=None, help="Filter by error code (contains).")
98+
@click.option("--message", type=str, default=None, help="Filter by message (contains).")
99+
@click.option(
100+
"--order-by",
101+
multiple=True,
102+
help=(
103+
"Order by field:direction (e.g., created_at:desc). Fields: created_at, "
104+
"updated_at, phase, from_status, to_status, result, attempts."
105+
),
106+
)
107+
def search(
108+
limit: int | None,
109+
offset: int | None,
110+
kernel_id: str | None,
111+
session_id: str | None,
112+
phase: str | None,
113+
from_status: tuple[str, ...],
114+
to_status: tuple[str, ...],
115+
result: str | None,
116+
error_code: str | None,
117+
message: str | None,
118+
order_by: tuple[str, ...],
119+
) -> None:
120+
"""Search kernel scheduling histories (superadmin only)."""
121+
from ai.backend.common.dto.manager.v2.scheduling_history.request import (
122+
AdminSearchKernelHistoriesInput,
123+
KernelHistoryOrder,
124+
)
125+
from ai.backend.common.dto.manager.v2.scheduling_history.types import KernelHistoryOrderField
126+
127+
history_filter = _build_kernel_history_filter(
128+
kernel_id=kernel_id,
129+
session_id=session_id,
130+
phase=phase,
131+
from_status=from_status,
132+
to_status=to_status,
133+
result=result,
134+
error_code=error_code,
135+
message=message,
136+
)
137+
138+
orders = (
139+
parse_order_options(order_by, KernelHistoryOrderField, KernelHistoryOrder)
140+
if order_by
141+
else None
142+
)
143+
144+
async def _run() -> None:
145+
registry = await create_v2_registry(load_v2_config())
146+
try:
147+
result_data = await registry.scheduling_history.search_kernel_history(
148+
AdminSearchKernelHistoriesInput(
149+
filter=history_filter,
150+
order=orders,
151+
limit=limit,
152+
offset=offset,
153+
),
154+
)
155+
print_result(result_data)
156+
finally:
157+
await registry.close()
158+
159+
asyncio.run(_run())
160+
161+
162+
@kernel.command(name="search-scoped")
163+
@click.argument("kernel_id", type=str)
164+
@click.option("--limit", type=int, default=None, help="Maximum items to return.")
165+
@click.option("--offset", type=int, default=None, help="Number of items to skip.")
166+
@click.option("--session-id", type=str, default=None, help="Filter by session ID (UUID).")
167+
@click.option("--phase", type=str, default=None, help="Filter by scheduling phase (contains).")
168+
@click.option(
169+
"--from-status",
170+
type=str,
171+
multiple=True,
172+
help="Filter by from_status values (repeatable).",
173+
)
174+
@click.option(
175+
"--to-status",
176+
type=str,
177+
multiple=True,
178+
help="Filter by to_status values (repeatable).",
179+
)
180+
@click.option("--result", type=_RESULT_CHOICES, default=None, help="Filter by scheduling result.")
181+
@click.option("--error-code", type=str, default=None, help="Filter by error code (contains).")
182+
@click.option("--message", type=str, default=None, help="Filter by message (contains).")
183+
@click.option(
184+
"--order-by",
185+
multiple=True,
186+
help=(
187+
"Order by field:direction (e.g., created_at:desc). Fields: created_at, "
188+
"updated_at, phase, from_status, to_status, result, attempts."
189+
),
190+
)
191+
def search_scoped(
192+
kernel_id: str,
193+
session_id: str | None,
194+
limit: int | None,
195+
offset: int | None,
196+
phase: str | None,
197+
from_status: tuple[str, ...],
198+
to_status: tuple[str, ...],
199+
result: str | None,
200+
error_code: str | None,
201+
message: str | None,
202+
order_by: tuple[str, ...],
203+
) -> None:
204+
"""Search kernel scheduling history scoped to KERNEL_ID."""
205+
from ai.backend.common.dto.manager.v2.rbac.types import UUIDScope
206+
from ai.backend.common.dto.manager.v2.scheduling_history.request import (
207+
KernelHistoryOrder,
208+
ScopedSearchKernelHistoriesInput,
209+
)
210+
from ai.backend.common.dto.manager.v2.scheduling_history.types import (
211+
KernelHistoryOrderField,
212+
KernelHistoryScopeDTO,
213+
)
214+
215+
scope = KernelHistoryScopeDTO(kernel=[UUIDScope(value=UUID(kernel_id))])
216+
217+
# The scope already narrows by kernel ID, so it is not repeated in the filter.
218+
history_filter = _build_kernel_history_filter(
219+
session_id=session_id,
220+
phase=phase,
221+
from_status=from_status,
222+
to_status=to_status,
223+
result=result,
224+
error_code=error_code,
225+
message=message,
226+
)
227+
228+
orders = (
229+
parse_order_options(order_by, KernelHistoryOrderField, KernelHistoryOrder)
230+
if order_by
231+
else None
232+
)
233+
234+
async def _run() -> None:
235+
registry = await create_v2_registry(load_v2_config())
236+
try:
237+
result_data = await registry.scheduling_history.kernel_scoped_search(
238+
ScopedSearchKernelHistoriesInput(
239+
scope=scope,
240+
filter=history_filter,
241+
order=orders,
242+
limit=limit,
243+
offset=offset,
244+
),
245+
)
246+
print_result(result_data)
247+
finally:
248+
await registry.close()
249+
250+
asyncio.run(_run())

src/ai/backend/client/v2/domains_v2/scheduling_history.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
from ai.backend.client.v2.base_domain import BaseDomainClient
88
from ai.backend.common.dto.manager.v2.scheduling_history.request import (
99
AdminSearchDeploymentHistoriesInput,
10+
AdminSearchKernelHistoriesInput,
1011
AdminSearchRouteHistoriesInput,
1112
AdminSearchSessionHistoriesInput,
13+
ScopedSearchKernelHistoriesInput,
1214
)
1315
from ai.backend.common.dto.manager.v2.scheduling_history.response import (
1416
AdminSearchDeploymentHistoriesPayload,
1517
AdminSearchRouteHistoriesPayload,
1618
AdminSearchSessionHistoriesPayload,
19+
SearchKernelHistoriesPayload,
1720
)
1821

1922
_PATH = "/v2/scheduling-history"
@@ -46,6 +49,30 @@ async def session_scoped_search(
4649
response_model=AdminSearchSessionHistoriesPayload,
4750
)
4851

52+
# ========== Kernel History ==========
53+
54+
async def search_kernel_history(
55+
self, request: AdminSearchKernelHistoriesInput
56+
) -> SearchKernelHistoriesPayload:
57+
"""Search kernel scheduling histories with admin scope."""
58+
return await self._client.typed_request(
59+
"POST",
60+
f"{_PATH}/kernels/admin/search",
61+
request=request,
62+
response_model=SearchKernelHistoriesPayload,
63+
)
64+
65+
async def kernel_scoped_search(
66+
self, request: ScopedSearchKernelHistoriesInput
67+
) -> SearchKernelHistoriesPayload:
68+
"""Search kernel scheduling histories within a session and/or kernel scope."""
69+
return await self._client.typed_request(
70+
"POST",
71+
f"{_PATH}/kernels/scoped/search",
72+
request=request,
73+
response_model=SearchKernelHistoriesPayload,
74+
)
75+
4976
# ========== Deployment History ==========
5077

5178
async def search_deployment_history(

0 commit comments

Comments
 (0)