ap-client 0.2.2.dev1__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.2.2.dev1
3
+ Version: 0.3.0
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pyyaml>=6.0
@@ -9,9 +9,9 @@ Requires-Dist: rich>=13.0.0
9
9
  Requires-Dist: typer>=0.9.0
10
10
  Requires-Dist: websockets>=13.0
11
11
  Provides-Extra: all
12
- Requires-Dist: instance-repo[oss]>=1.0.9; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]>=0.7.0; extra == 'all'
13
13
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]>=1.0.9; extra == 'dataset'
14
+ Requires-Dist: instance-repo[oss]>=0.7.0; extra == 'dataset'
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.
@@ -293,48 +293,6 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
293
293
  return {"workspace_id": workspace_id} if workspace_id else None
294
294
 
295
295
 
296
- def _unwrap_data(payload: Any) -> Any:
297
- """Unwrap the apiserver ``{code,message,data}`` envelope when present.
298
-
299
- The dataset-domain detail/create/patch endpoints return a bare JSON body,
300
- but the same handlers are occasionally wrapped by the generic success
301
- envelope. Tolerate both so command code never has to branch: a mapping that
302
- carries ``data`` alongside ``code``/``message`` is treated as an envelope,
303
- anything else is returned verbatim.
304
- """
305
- if (
306
- isinstance(payload, dict)
307
- and "data" in payload
308
- and ("code" in payload or "message" in payload)
309
- ):
310
- return payload["data"]
311
- return payload
312
-
313
-
314
- def _split_paged(payload: Any) -> tuple[list[dict], dict]:
315
- """Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
316
-
317
- ``PagedSuccess`` is ``{code,message,data:[...],pagination:{total,page,page_size}}``.
318
- A bare list (or a null ``data``) yields an empty pagination dict so callers
319
- can render results without probing the response shape.
320
- """
321
- if isinstance(payload, list):
322
- return [item for item in payload if isinstance(item, dict)], {}
323
- if not isinstance(payload, dict):
324
- return [], {}
325
- records = payload.get("data")
326
- pagination = payload.get("pagination")
327
- return (
328
- [item for item in (records or []) if isinstance(item, dict)],
329
- pagination if isinstance(pagination, dict) else {},
330
- )
331
-
332
-
333
- def _as_record_list(payload: Any) -> list[dict]:
334
- """Coerce a list-returning endpoint to ``list[dict]``, envelope or not."""
335
- return _split_paged(payload)[0]
336
-
337
-
338
296
  class APIClient:
339
297
  """Agent Platform API client."""
340
298
 
@@ -750,35 +708,8 @@ class APIClient:
750
708
  return self._get(f"/templates/{quote(name)}", params=params)
751
709
 
752
710
  def list_benchmarks(self) -> list:
753
- """List all benchmarks from the central registry, preserving legacy IDs."""
754
- benchmarks = []
755
- page = 1
756
- page_size = 200
757
- # 与 CLI dataset list 的自动翻页同口径:100 页上限,防分页异常导致死循环。
758
- max_pages = 100
759
- while page <= max_pages:
760
- payload = self._get(
761
- "/apis/v1/benchmarks", params={"page": page, "page_size": page_size}
762
- )
763
- records, pagination = _split_paged(payload)
764
- for record in records:
765
- item = dict(record)
766
- if "source_id" in item:
767
- item.setdefault("id", item["source_id"])
768
- benchmarks.append(item)
769
- if not records or not pagination:
770
- break
771
- try:
772
- total = int(pagination.get("total"))
773
- except (TypeError, ValueError):
774
- total = None
775
- if total is not None:
776
- if len(benchmarks) >= total:
777
- break
778
- elif len(records) < page_size:
779
- break
780
- page += 1
781
- return benchmarks
711
+ """List all active benchmarks from the local benchmark registry."""
712
+ return self._get("/benchmarks")
782
713
 
783
714
  def get_benchmark(self, name: str) -> dict:
784
715
  """Get a single benchmark by exact name."""
@@ -913,189 +844,6 @@ class APIClient:
913
844
  "instance_ids": all_instance_ids,
914
845
  }
915
846
 
916
- # ==================== Dataset series (apiserver dataset domain) ====================
917
- #
918
- # 这一节对接 Go apiserver 的 dataset 领域(/apis/v1/datasets/...),与上面
919
- # ossdata 目录的 /api/datasets 路由完全无关。方法名统一带 `_series`,
920
- # 以免和 `ap job create --dataset` 依赖的 list_all_datasets /
921
- # list_dataset_versions / list_all_dataset_instances 混淆。
922
- #
923
- # 列表端点返回 PagedSuccess envelope:
924
- # {"code":0,"message":"ok","data":[...],"pagination":{"total","page","page_size"}}
925
- # 详情/创建/更新端点返回裸 JSON body。
926
-
927
- _DATASET_SERIES_BASE = "/apis/v1/datasets/series"
928
- _DATASET_VERSIONS_BASE = "/apis/v1/datasets/versions"
929
- _DATASET_INSTANCES_BASE = "/apis/v1/datasets/instances"
930
-
931
- def list_dataset_series_page(
932
- self,
933
- *,
934
- q: Optional[str] = None,
935
- keyword: Optional[str] = None,
936
- visibility: Optional[str] = None,
937
- benchmark_id: Optional[str] = None,
938
- include_deprecated: bool = False,
939
- environment: Optional[str] = None,
940
- page: int = 1,
941
- page_size: int = 20,
942
- ) -> dict:
943
- """一页 dataset series(原始 PagedSuccess envelope)。
944
-
945
- 服务端没有 owner/mine/l1 过滤参数,调用方需要时只能在客户端做页内过滤。
946
- """
947
- params: dict = {"page": page, "page_size": page_size}
948
- if q:
949
- params["q"] = q
950
- if keyword:
951
- params["keyword"] = keyword
952
- if visibility:
953
- params["visibility"] = visibility
954
- if benchmark_id:
955
- params["benchmark_id"] = benchmark_id
956
- if include_deprecated:
957
- params["include_deprecated"] = "true"
958
- if environment:
959
- params["environment"] = environment
960
- return self._get(self._DATASET_SERIES_BASE, params=params)
961
-
962
- def list_dataset_series(self, **kw) -> tuple[list[dict], dict]:
963
- """列出 dataset series,返回 ``(records, pagination)``。"""
964
- return _split_paged(self.list_dataset_series_page(**kw))
965
-
966
- def get_dataset_series(self, dataset_name: str, *, environment: Optional[str] = None) -> dict:
967
- """按名字取 dataset series 详情(含 dataset_id / claimable / stats)。"""
968
- params: dict = {"dataset_name": dataset_name}
969
- if environment:
970
- params["environment"] = environment
971
- return _unwrap_data(self._get(f"{self._DATASET_SERIES_BASE}/detail", params=params))
972
-
973
- def update_dataset_series(self, dataset_name: str, body: dict) -> dict:
974
- """PATCH dataset series 元数据(visibility/status 需要 dataset-admin)。"""
975
- return _unwrap_data(
976
- self._request(
977
- "PATCH",
978
- f"{self._DATASET_SERIES_BASE}/detail",
979
- params={"dataset_name": dataset_name},
980
- json_body=body,
981
- )
982
- )
983
-
984
- def list_claim_workspaces(self) -> list[dict]:
985
- """列出当前用户可用于认领的 workspace。"""
986
- return _as_record_list(self._get("/apis/v1/datasets/claim-workspaces"))
987
-
988
- # ---- dataset versions ----
989
-
990
- def list_dataset_series_versions(
991
- self,
992
- dataset_name: str,
993
- *,
994
- status: Optional[str] = None,
995
- page: int = 1,
996
- page_size: int = 20,
997
- environment: Optional[str] = None,
998
- ) -> tuple[list[dict], dict]:
999
- """列出某 dataset 的版本,返回 ``(records, pagination)``。列表项不含 splits 数组。"""
1000
- params: dict = {"dataset_name": dataset_name, "page": page, "page_size": page_size}
1001
- if status:
1002
- params["status"] = status
1003
- if environment:
1004
- params["environment"] = environment
1005
- return _split_paged(self._get(self._DATASET_VERSIONS_BASE, params=params))
1006
-
1007
- def get_dataset_series_version(
1008
- self,
1009
- dataset_name: str,
1010
- version: str,
1011
- *,
1012
- environment: Optional[str] = None,
1013
- ) -> dict:
1014
- """取单个版本详情(含 run_type / splits[] / manifest / published_at)。
1015
-
1016
- ``version=""`` 表示 split_first 的**无版本空间**,原样下发空串——绝不隐式取 latest。
1017
- """
1018
- params: dict = {"dataset_name": dataset_name, "version": version}
1019
- if environment:
1020
- params["environment"] = environment
1021
- return _unwrap_data(self._get(f"{self._DATASET_VERSIONS_BASE}/detail", params=params))
1022
-
1023
- # ---- dataset instances (metadata only) ----
1024
-
1025
- def list_dataset_series_instances(
1026
- self,
1027
- dataset_name: str,
1028
- *,
1029
- version: Optional[str] = None,
1030
- split: Optional[str] = None,
1031
- instance_id: Optional[str] = None,
1032
- page: int = 1,
1033
- page_size: int = 50,
1034
- environment: Optional[str] = None,
1035
- ) -> tuple[list[dict], dict]:
1036
- """列出 instance 元数据,返回 ``(records, pagination)``。
1037
-
1038
- ``version=""`` / ``split=""`` 原样下发(无版本空间语义),``None`` 才省略该参数。
1039
- """
1040
- params: dict = {"dataset_name": dataset_name, "page": page, "page_size": page_size}
1041
- if version is not None:
1042
- params["version"] = version
1043
- if split is not None:
1044
- params["split"] = split
1045
- if instance_id:
1046
- params["instance_id"] = instance_id
1047
- if environment:
1048
- params["environment"] = environment
1049
- return _split_paged(self._get(self._DATASET_INSTANCES_BASE, params=params))
1050
-
1051
- def get_dataset_series_instance(
1052
- self,
1053
- dataset_name: str,
1054
- version: str,
1055
- split: str,
1056
- instance_id: str,
1057
- *,
1058
- environment: Optional[str] = None,
1059
- ) -> dict:
1060
- """取单个 instance 的元数据详情。``version=""`` 表示无版本空间。"""
1061
- params: dict = {
1062
- "dataset_name": dataset_name,
1063
- "version": version,
1064
- "split": split,
1065
- "instance_id": instance_id,
1066
- }
1067
- if environment:
1068
- params["environment"] = environment
1069
- return _unwrap_data(self._get(f"{self._DATASET_INSTANCES_BASE}/detail", params=params))
1070
-
1071
- def get_dataset_series_split(self, dataset_name: str, version: str, split: str) -> dict:
1072
- """Read the selected split's lifecycle without a version-level aggregate."""
1073
- return _unwrap_data(
1074
- self._get(
1075
- "/apis/v1/datasets/splits/detail",
1076
- params={"dataset_name": dataset_name, "version": version, "split": split},
1077
- )
1078
- )
1079
-
1080
- def get_dataset_split_release(self, workflow_id: str) -> dict:
1081
- """Read a release workflow, including approval and per-step outcomes."""
1082
- return _unwrap_data(self._get(f"/apis/v1/workflows/{quote(workflow_id, safe='')}"))
1083
-
1084
- # ---- permissions ----
1085
-
1086
- def get_my_resource_permissions(self, resource_type: str, resource_id: str) -> list[str]:
1087
- """当前用户在某资源上的 action 列表(``{"actions":[...]}``)。"""
1088
- payload = self._get(
1089
- "/apis/v1/me/resource-permissions",
1090
- params={"resource_type": resource_type, "resource_id": resource_id},
1091
- )
1092
- payload = _unwrap_data(payload)
1093
- if isinstance(payload, dict):
1094
- actions = payload.get("actions")
1095
- if isinstance(actions, list):
1096
- return [str(action) for action in actions]
1097
- return []
1098
-
1099
847
  # ==================== Meta operations ====================
1100
848
 
1101
849
  def list_meta_models(self) -> dict:
@@ -1243,6 +991,7 @@ class APIClient:
1243
991
  runner: Optional[str] = None,
1244
992
  runner_options: Optional[dict] = None,
1245
993
  idempotency_key: Optional[str] = None,
994
+ max_failure_retries: int = 0,
1246
995
  # ─── Scheduling hints ─────────────────────────────────────────
1247
996
  priority: Optional[str] = None,
1248
997
  priority_weight: Optional[int] = None,
@@ -1295,6 +1044,7 @@ class APIClient:
1295
1044
  runner=runner,
1296
1045
  runner_options=runner_options,
1297
1046
  idempotency_key=idempotency_key,
1047
+ max_failure_retries=max_failure_retries,
1298
1048
  priority=priority,
1299
1049
  priority_weight=priority_weight,
1300
1050
  resource_mode=resource_mode,
@@ -1345,6 +1095,7 @@ class APIClient:
1345
1095
  runner: Optional[str] = None,
1346
1096
  runner_options: Optional[dict] = None,
1347
1097
  idempotency_key: Optional[str] = None,
1098
+ max_failure_retries: int = 0,
1348
1099
  # ─── Scheduling hints ─────────────────────────────────────────
1349
1100
  priority: Optional[str] = None,
1350
1101
  priority_weight: Optional[int] = None,
@@ -1431,6 +1182,8 @@ class APIClient:
1431
1182
  body["runner_options"] = runner_options
1432
1183
  if idempotency_key is not None:
1433
1184
  body["idempotency_key"] = idempotency_key
1185
+ if max_failure_retries:
1186
+ body["max_failure_retries"] = max_failure_retries
1434
1187
 
1435
1188
  # Scheduling hints
1436
1189
  if priority is not None:
@@ -22,10 +22,8 @@ from ap_client.api import (
22
22
  set_verbose_override,
23
23
  )
24
24
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
25
- from ap_client.dataset_commands import register as _register_dataset_commands
26
25
  from ap_client.exporter import export_group, export_job
27
- from ap_client.fs_commands import register as _register_fs_commands
28
- from ap_client.instance_commands import register as _register_instance_commands
26
+ from ap_client.irepo_commands import register as _register_dataset_repo
29
27
  from ap_client.profile_commands import profile_app
30
28
  from ap_client.waiter import (
31
29
  WaitTimeoutError,
@@ -146,11 +144,8 @@ app.add_typer(meta_job_type_app, name="meta-job-type")
146
144
  app.add_typer(benchmark_app, name="benchmark")
147
145
  app.add_typer(checkpoint_app, name="checkpoint")
148
146
 
149
- # Dataset 领域(见 docs/ap-cli-dataset-refactor-design.md):
150
- # ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
151
- _register_dataset_commands(dataset_app)
152
- _register_instance_commands(app)
153
- _register_fs_commands(app)
147
+ # ap dataset repo:透传 instance_repo CLI(见 ap_client/irepo_commands.py)
148
+ _register_dataset_repo(dataset_app)
154
149
 
155
150
  _PAI_RUNTIME_ENV_TAGS: tuple[tuple[str, str], ...] = (
156
151
  ("DLC_JOB_ID", "dlc_job_id"),
@@ -210,6 +205,7 @@ def _build_job_create_retry_command(
210
205
  concurrency: Optional[int],
211
206
  batch_size: int,
212
207
  trials: Optional[int] = None,
208
+ max_failure_retries: int = 0,
213
209
  wait: bool,
214
210
  wait_interval: float,
215
211
  wait_timeout: Optional[float],
@@ -261,6 +257,8 @@ def _build_job_create_retry_command(
261
257
  args.extend(["--batch-size", str(batch_size)])
262
258
  if trials is not None:
263
259
  args.extend(["--trials", str(trials)])
260
+ if max_failure_retries:
261
+ args.extend(["--max-failure-retries", str(max_failure_retries)])
264
262
  if wait:
265
263
  args.append("--wait")
266
264
  if wait_interval != 5.0:
@@ -929,7 +927,10 @@ def _print_params_plain(params: Any, masked_params: list) -> None:
929
927
  rows: list[tuple[str, Any]] = []
930
928
  for key, value in params.items():
931
929
  display_value = _format_plain_value(value)
932
- if str(key) in masked_paths and value == "***":
930
+ # 直接信任 masked_params 路径:服务端掩码形态已扩展为保留首尾(如
931
+ # sk-***890 / ****),不能再按值严格等于 "***" 判断,否则形态变化后
932
+ # (masked) 标识静默丢失
933
+ if str(key) in masked_paths:
933
934
  display_value = f"{display_value} (masked)"
934
935
  rows.append((str(key), display_value))
935
936
  _print_key_values(rows, skip_empty=False)
@@ -1083,6 +1084,7 @@ def _idempotency_item_key(
1083
1084
  queue: Optional[str],
1084
1085
  account_pool: Optional[str],
1085
1086
  resource_profile_id: Optional[str] = None,
1087
+ max_failure_retries: int = 0,
1086
1088
  ) -> str:
1087
1089
  # Client-supplied item keys are authoritative; server hashing is only a
1088
1090
  # fallback for non-CLI callers, so this payload intentionally need not
@@ -1102,6 +1104,8 @@ def _idempotency_item_key(
1102
1104
  }
1103
1105
  if resource_profile_id is not None:
1104
1106
  payload_data["resource_profile_id"] = resource_profile_id
1107
+ if max_failure_retries:
1108
+ payload_data["max_failure_retries"] = max_failure_retries
1105
1109
  payload = json.dumps(
1106
1110
  payload_data,
1107
1111
  sort_keys=True,
@@ -2020,17 +2024,24 @@ def template_fetch(
2020
2024
 
2021
2025
  # ==================== Dataset operations ====================
2022
2026
 
2023
- # 设计 §6 / 上游 §13.2:`ap dataset versions` 与 `ap dataset instances` 行为保持不变
2024
- # (仍打 ossdata 路由,`ap job create` 的配套流程依赖它们),只追加固定弃用文案。
2025
- # 文案**只写 stderr**——JSON/YAML 的 stdout 必须仍可被 json.loads 直接消费。
2026
- _DATASET_VERSIONS_DEPRECATION = (
2027
- "Warning: `ap dataset versions` is deprecated.\n"
2028
- "Use `ap dataset version list --dataset <dataset>` instead."
2029
- )
2030
- _DATASET_INSTANCES_DEPRECATION = (
2031
- "Warning: `ap dataset instances` is deprecated.\n"
2032
- "Use `ap instance list --dataset <dataset> --split <split> [--version <version>]` instead."
2033
- )
2027
+
2028
+ @dataset_app.command("list")
2029
+ def dataset_list(
2030
+ search: Optional[str] = typer.Argument(None, help="Search keyword"),
2031
+ output_format: str = typer.Option(
2032
+ None,
2033
+ "--format",
2034
+ help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2035
+ ),
2036
+ ):
2037
+ """List all datasets."""
2038
+ output_format = _normalize_output_format(output_format, keep_table=True)
2039
+ client = get_client()
2040
+ result = client.list_all_datasets(search)
2041
+ if output_format == "table":
2042
+ _print_records_table(result)
2043
+ else:
2044
+ _print_formatted(result, output_format)
2034
2045
 
2035
2046
 
2036
2047
  @dataset_app.command("versions")
@@ -2042,9 +2053,8 @@ def dataset_versions(
2042
2053
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2043
2054
  ),
2044
2055
  ):
2045
- """List dataset versions (deprecated: use 'ap dataset version list')."""
2056
+ """List dataset versions."""
2046
2057
  output_format = _normalize_output_format(output_format, keep_table=True)
2047
- _emit_progress(_DATASET_VERSIONS_DEPRECATION)
2048
2058
  client = get_client()
2049
2059
  versions = client.list_dataset_versions(dataset)
2050
2060
  if output_format == "table":
@@ -2064,9 +2074,8 @@ def dataset_instances(
2064
2074
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2065
2075
  ),
2066
2076
  ):
2067
- """List dataset instances (deprecated: use 'ap instance list')."""
2077
+ """List dataset instances."""
2068
2078
  output_format = _normalize_output_format(output_format, keep_table=True)
2069
- _emit_progress(_DATASET_INSTANCES_DEPRECATION)
2070
2079
  client = get_client()
2071
2080
  result = client.list_all_dataset_instances(dataset_version)
2072
2081
  if output_format == "table":
@@ -2886,6 +2895,12 @@ def job_create(
2886
2895
  "--idempotency-key",
2887
2896
  help="UUID4 key for retrying the same logical submission; providing one enables idempotency",
2888
2897
  ),
2898
+ max_failure_retries: int = typer.Option(
2899
+ 0,
2900
+ "--max-failure-retries",
2901
+ min=0,
2902
+ help="Maximum automatic retries after each Job fails (default: 0)",
2903
+ ),
2889
2904
  dry_run: bool = typer.Option(
2890
2905
  False,
2891
2906
  "--dry-run",
@@ -3227,6 +3242,7 @@ def job_create(
3227
3242
  runner=runner,
3228
3243
  runner_options=runner_options_dict,
3229
3244
  idempotency_key=submission_idempotency_key,
3245
+ max_failure_retries=max_failure_retries,
3230
3246
  group_id=group_id,
3231
3247
  model_base_url_collection=model_base_url_collection_list,
3232
3248
  group_post_process=group_post_process_dict,
@@ -3281,6 +3297,7 @@ def job_create(
3281
3297
  concurrency=concurrency,
3282
3298
  batch_size=batch_size,
3283
3299
  trials=trials,
3300
+ max_failure_retries=max_failure_retries,
3284
3301
  wait=wait,
3285
3302
  wait_interval=wait_interval,
3286
3303
  wait_timeout=wait_timeout,
@@ -3328,6 +3345,7 @@ def job_create(
3328
3345
  runner=runner,
3329
3346
  runner_options=runner_options_dict,
3330
3347
  idempotency_key=submission_idempotency_key,
3348
+ max_failure_retries=max_failure_retries,
3331
3349
  group_id=group_id,
3332
3350
  model_base_url_collection=model_base_url_collection_list,
3333
3351
  priority=priority,
@@ -3521,6 +3539,7 @@ def job_create(
3521
3539
  overrides=overrides,
3522
3540
  concurrency=concurrency,
3523
3541
  batch_size=batch_size,
3542
+ max_failure_retries=max_failure_retries,
3524
3543
  wait=wait,
3525
3544
  wait_interval=wait_interval,
3526
3545
  wait_timeout=wait_timeout,
@@ -3630,6 +3649,7 @@ def job_create(
3630
3649
  queue=queue,
3631
3650
  account_pool=account_pool,
3632
3651
  resource_profile_id=resource_profile_id,
3652
+ max_failure_retries=max_failure_retries,
3633
3653
  )
3634
3654
  batch.append(item)
3635
3655
  return batch
@@ -3653,6 +3673,7 @@ def job_create(
3653
3673
  enable_otel_tracing=enable_otel_tracing,
3654
3674
  runner=runner,
3655
3675
  runner_options=runner_options_dict,
3676
+ max_failure_retries=max_failure_retries,
3656
3677
  priority=priority,
3657
3678
  priority_weight=priority_weight,
3658
3679
  resource_mode=resource_mode,
@@ -3694,6 +3715,7 @@ def job_create(
3694
3715
  runner=runner,
3695
3716
  runner_options=runner_options_dict,
3696
3717
  idempotency_key=submission_idempotency_key,
3718
+ max_failure_retries=max_failure_retries,
3697
3719
  priority=priority,
3698
3720
  priority_weight=priority_weight,
3699
3721
  resource_mode=resource_mode,
@@ -3749,6 +3771,7 @@ def job_create(
3749
3771
  runner=runner,
3750
3772
  runner_options=runner_options_dict,
3751
3773
  idempotency_key=submission_idempotency_key,
3774
+ max_failure_retries=max_failure_retries,
3752
3775
  priority=priority,
3753
3776
  priority_weight=priority_weight,
3754
3777
  resource_mode=resource_mode,
@@ -3843,6 +3866,7 @@ def job_create(
3843
3866
  enable_otel_tracing=enable_otel_tracing,
3844
3867
  runner=runner,
3845
3868
  runner_options=runner_options_dict,
3869
+ max_failure_retries=max_failure_retries,
3846
3870
  priority=priority,
3847
3871
  priority_weight=priority_weight,
3848
3872
  resource_mode=resource_mode,
@@ -3878,6 +3902,7 @@ def job_create(
3878
3902
  runner=runner,
3879
3903
  runner_options=runner_options_dict,
3880
3904
  idempotency_key=submission_idempotency_key,
3905
+ max_failure_retries=max_failure_retries,
3881
3906
  priority=priority,
3882
3907
  priority_weight=priority_weight,
3883
3908
  resource_mode=resource_mode,
@@ -3923,6 +3948,7 @@ def job_create(
3923
3948
  runner=runner,
3924
3949
  runner_options=runner_options_dict,
3925
3950
  idempotency_key=submission_idempotency_key,
3951
+ max_failure_retries=max_failure_retries,
3926
3952
  priority=priority,
3927
3953
  priority_weight=priority_weight,
3928
3954
  resource_mode=resource_mode,
@@ -4779,7 +4805,7 @@ def benchmark_list(
4779
4805
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
4780
4806
  ),
4781
4807
  ):
4782
- """List benchmarks from the central benchmark registry."""
4808
+ """List benchmarks from the local benchmark registry."""
4783
4809
  output_format = _normalize_output_format(output_format, keep_table=True)
4784
4810
  client = get_client()
4785
4811
  result = client.list_benchmarks()