|
| 1 | +# SDK 支持 inference_v3 `config_file` |
| 2 | + |
| 3 | +日期:2026-05-21 |
| 4 | +仓库:`centml-python-client` |
| 5 | +新建分支:`honglin/sdk-config-file-support` |
| 6 | + |
| 7 | +## 背景 |
| 8 | + |
| 9 | +平台侧两个 PR 为 `inference_v3` deployment 增加了可选的单文件挂载支持: |
| 10 | + |
| 11 | +- [`#3656`](https://github.com/CentML/platform/pull/3656) —— schema、catalog、 |
| 12 | + generated clients、UI 全套:`CreateInferenceV3DeploymentRequest.config_file` 和 |
| 13 | + `GetInferenceV3DeploymentResponse.config_file` 携带一个可选的 |
| 14 | + `ConfigFileMount{filename, mount_path, content}`。 |
| 15 | +- [`#3667`](https://github.com/CentML/platform/pull/3667) —— 收紧 server 端校验: |
| 16 | + `content` 字段 64 KiB UTF-8 字节上限、`mount_path` 拒绝 `..` 段、 |
| 17 | + PATH_MAX 按 UTF-8 字节计。 |
| 18 | + |
| 19 | +`centml-python-client` SDK 当前在 `requirements.txt` pin 了 |
| 20 | +`platform-api-python-client==4.9.0`,早于这两个 PR。 |
| 21 | +PyPI 现在 latest 是 `platform-api-python-client==4.10.0` |
| 22 | +(2026-05-21 用 `pip index versions` 验证过),其中含 |
| 23 | +`platform_api_python_client/models/config_file_mount.py` |
| 24 | +(通过解包 wheel 验证)。 |
| 25 | + |
| 26 | +由于 pin 版本里没有这个 model,SDK 用户今天没法传 `config_file`。 |
| 27 | +bump pin 之后,`config_file` 作为 `CreateInferenceV3DeploymentRequest` |
| 28 | +的一个透传字段就能用,SDK 代码无需任何变动 —— 但每次使用都需要用户自己 |
| 29 | +从磁盘读文件构造 `ConfigFileMount`,这个动作是高频重复的。 |
| 30 | + |
| 31 | +## 目标 |
| 32 | + |
| 33 | +用最小表面积让 `config_file` 在 SDK 中成为一等公民: |
| 34 | + |
| 35 | +1. Bump generated client pin,让 `ConfigFileMount` 可被 import。 |
| 36 | +2. 加一个轻量 helper,从磁盘读文件并返回填好的 `ConfigFileMount`, |
| 37 | + 匹配仓内 `centml/sdk/utils/client_certs.py` 现有 pattern。 |
| 38 | +3. 更新 canonical example,让用户看到 helper 的实际用法。 |
| 39 | + |
| 40 | +非目标: |
| 41 | + |
| 42 | +- 不加 CLI 命令(2026-05-21 用户确认 —— CLI 延后)。 |
| 43 | +- 不在 client 侧重复 server 校验(size cap、path 规则)。 |
| 44 | + API 是 source of truth,server 限制可能变。 |
| 45 | +- 不对 generated `ConfigFileMount` 做 wrapper / subclass |
| 46 | + (仓内没有 subclass generated model 的先例)。 |
| 47 | +- 不在 `centml.sdk` 顶层 re-export:与 |
| 48 | + `centml/sdk/utils/client_certs.py` 一致,走 qualified path 导入。 |
| 49 | + |
| 50 | +## 沿用的仓内既有约定 |
| 51 | + |
| 52 | +- `centml/sdk/utils/client_certs.py` —— free function(动词命名,如 |
| 53 | + `generate_ca_client_triplet`、`save_pem_file`),不对 generated model |
| 54 | + 做 class wrapping,函数上方 `#` 块注释解释 *why*。 |
| 55 | +- `examples/sdk/create_inference.py` —— 单 `main()`,文件顶部 |
| 56 | + `import centml` + `from centml.sdk.api import get_centml_client` + |
| 57 | + `from centml.sdk import ...`,文件末尾用三引号注释 pause/delete 示例。 |
| 58 | +- `requirements.txt` —— 严格 `==` pinning。 |
| 59 | +- `scripts/format.sh` —— Black,`--skip-string-normalization`, |
| 60 | + `--skip-magic-trailing-comma`,`--line-length 120`。 |
| 61 | +- `tests/` —— 文件名 `test_<area>.py`,pytest,conftest 带 `--sanity` 选项; |
| 62 | + 无 E2E 网络调用。 |
| 63 | + |
| 64 | +## 设计 |
| 65 | + |
| 66 | +### 1. 依赖 bump |
| 67 | + |
| 68 | +`requirements.txt`: |
| 69 | + |
| 70 | +```diff |
| 71 | +-platform-api-python-client==4.9.0 |
| 72 | ++platform-api-python-client==4.10.0 |
| 73 | +``` |
| 74 | + |
| 75 | +理由:4.10.0 是 PyPI 当前 latest,同时包含 feature PR #3656 和校验收紧 PR |
| 76 | +#3667。已通过下载 wheel 验证。 |
| 77 | + |
| 78 | +无其他依赖变动。`setup.py` 从 `requirements.txt` 读,无需修改。 |
| 79 | + |
| 80 | +### 2. Helper |
| 81 | + |
| 82 | +新文件 `centml/sdk/utils/config_file.py`: |
| 83 | + |
| 84 | +```python |
| 85 | +import os |
| 86 | +from typing import Optional |
| 87 | + |
| 88 | +from platform_api_python_client import ConfigFileMount |
| 89 | + |
| 90 | + |
| 91 | +# Load a file off disk into a ConfigFileMount. Field-level validation |
| 92 | +# (size cap, filename charset, mount_path rules) is intentionally left |
| 93 | +# to the API so SDK doesn't drift when server limits change. |
| 94 | +def load_config_file_mount(path: str, mount_path: str, filename: Optional[str] = None) -> ConfigFileMount: |
| 95 | + with open(path, "r", encoding="utf-8") as f: |
| 96 | + content = f.read() |
| 97 | + return ConfigFileMount( |
| 98 | + filename=filename or os.path.basename(path), |
| 99 | + mount_path=mount_path, |
| 100 | + content=content, |
| 101 | + ) |
| 102 | +``` |
| 103 | + |
| 104 | +设计决定,附理由: |
| 105 | + |
| 106 | +- **Free function,不是 subclass。** 匹配 `client_certs.py`;仓内没有 |
| 107 | + subclass generated client model 的先例。 |
| 108 | +- **直接返回 generated `ConfigFileMount`。** Type signature 直接 drop in |
| 109 | + `CreateInferenceV3DeploymentRequest(config_file=...)`,无需转换。 |
| 110 | +- **`filename` 可选,默认 `os.path.basename(path)`。** 常见情形就是 |
| 111 | + 把 `nginx.conf` 挂载为 `nginx.conf`;需要 rename 时显式传。 |
| 112 | +- **`mount_path` 无默认值。** 容器内挂载位置正是这个字段的存在意义 —— |
| 113 | + 没有合理默认。 |
| 114 | +- **不做 client-side 校验。** 2026-05-21 用户确认:API 是 source of |
| 115 | + truth,server 侧限制(如 64 KiB content 上限)可能演化。Server 在 |
| 116 | + 超限时返回 422 并带具体字节数(见 #3667)。 |
| 117 | +- **UTF-8 文本读取。** Server 契约是 `content: str`。二进制 config |
| 118 | + 不在当前 platform schema 范围内。 |
| 119 | +- **不改 `centml/sdk/utils/__init__.py`。** 该文件当前为空; |
| 120 | + 消费者用 qualified path 导入 |
| 121 | + `from centml.sdk.utils.config_file import load_config_file_mount`, |
| 122 | + 与 `client_certs.py` 的消费方式一致。 |
| 123 | + |
| 124 | +函数命名 `load_config_file_mount`。"Load" 是"从磁盘读+构造"的精确动词; |
| 125 | +`build_*` 因为项目规则偏好精确动词、不推荐 `build` 而被淘汰。 |
| 126 | + |
| 127 | +### 3. Example 更新 |
| 128 | + |
| 129 | +`examples/sdk/create_inference.py` —— 增加 helper import,并在原有 request |
| 130 | +上加 `config_file=...` 字段。其他字段全部保留。最终形态: |
| 131 | + |
| 132 | +```python |
| 133 | +import centml |
| 134 | +from centml.sdk.api import get_centml_client |
| 135 | +from centml.sdk import DeploymentType, CreateInferenceV3DeploymentRequest, UserVaultType |
| 136 | +from centml.sdk.utils.config_file import load_config_file_mount |
| 137 | + |
| 138 | + |
| 139 | +def main(): |
| 140 | + with get_centml_client() as cclient: |
| 141 | + certs = cclient.get_user_vault(UserVaultType.CERTIFICATES) |
| 142 | + |
| 143 | + request = CreateInferenceV3DeploymentRequest( |
| 144 | + name="nginx", |
| 145 | + cluster_id=1000, |
| 146 | + hardware_instance_id=1000, |
| 147 | + image_url="nginxinc/nginx-unprivileged", |
| 148 | + port=8080, |
| 149 | + min_replicas=1, |
| 150 | + max_replicas=3, |
| 151 | + initial_replicas=1, |
| 152 | + endpoint_certificate_authority=certs["my_cert"], |
| 153 | + max_surge=1, |
| 154 | + max_unavailable=0, |
| 155 | + healthcheck="/", |
| 156 | + concurrency=10, |
| 157 | + # Helper reads ./nginx.conf and wraps it; pass an inline |
| 158 | + # ConfigFileMount(filename=..., mount_path=..., content=...) if |
| 159 | + # the content is already in memory. |
| 160 | + config_file=load_config_file_mount( |
| 161 | + path="./nginx.conf", |
| 162 | + mount_path="/etc/nginx/conf.d/default.conf", |
| 163 | + ), |
| 164 | + ) |
| 165 | + response = cclient.create_inference(request) |
| 166 | + print("Create deployment response: ", response) |
| 167 | + |
| 168 | + deployment = cclient.get_inference(response.id) |
| 169 | + print("Deployment details: ", deployment) |
| 170 | + |
| 171 | + ''' |
| 172 | + ### Pause the deployment |
| 173 | + cclient.pause(deployment.id) |
| 174 | +
|
| 175 | + ### Delete the deployment |
| 176 | + cclient.delete(deployment.id) |
| 177 | + ''' |
| 178 | + |
| 179 | + |
| 180 | +if __name__ == "__main__": |
| 181 | + main() |
| 182 | +``` |
| 183 | + |
| 184 | +末尾三引号 pause/delete 块原样保留。 |
| 185 | + |
| 186 | +### 4. 测试 |
| 187 | + |
| 188 | +新文件 `tests/test_sdk_config_file_helper.py`。测试先于 helper 实现写 |
| 189 | +(按项目 Prime Directive #2 的 TDD 要求)。无网络、无 platform mock —— |
| 190 | +只用 `tmp_path` fixture + pytest。 |
| 191 | + |
| 192 | +测试用例(每个一个 `def test_*`): |
| 193 | + |
| 194 | +1. **`test_default_filename_from_basename`**:写 `tmp_path/nginx.conf`, |
| 195 | + 调 `load_config_file_mount(path, "/etc/nginx/conf.d/default.conf")`, |
| 196 | + 断言 `mount.filename == "nginx.conf"`、`mount.content == <文件内容>`、 |
| 197 | + `mount.mount_path == "/etc/nginx/conf.d/default.conf"`。 |
| 198 | + |
| 199 | +2. **`test_explicit_filename_overrides_basename`**:写 `tmp_path/local.txt`, |
| 200 | + 传 `filename="remote.conf"`,断言 `mount.filename == "remote.conf"`, |
| 201 | + content / mount_path 保持。 |
| 202 | + |
| 203 | +3. **`test_utf8_multibyte_content_roundtrips`**:写一个含 CJK 字符的文件, |
| 204 | + 断言 `mount.content` 与原始字符串相等(catch 错误的 binary-mode 读)。 |
| 205 | + |
| 206 | +4. **`test_missing_file_raises_filenotfound`**:传一个不存在的 path, |
| 207 | + 断言 `FileNotFoundError`。明确 helper *不* 吞掉 I/O 错误。 |
| 208 | + |
| 209 | +测试不断言 server 侧限制(size cap、mount_path 规则)—— 那些是 server |
| 210 | +契约,不是 helper 的职责(见设计 / "不做 client-side 校验")。 |
| 211 | + |
| 212 | +### 5. 范围之外 |
| 213 | + |
| 214 | +- 不加 inference create/update 的 CLI 命令(按用户 2026-05-21 决定延后)。 |
| 215 | +- 不动 `README.md`,除非已有 SDK 用法 doc 需要更新。 |
| 216 | +- 不动 `centml/sdk/api.py` —— `create_inference` / `update_inference` |
| 217 | + 已经吃 `CreateInferenceV3DeploymentRequest`,pass-through 就够用。 |
| 218 | +- 不加 in-memory helper(如 `load_config_file_mount_from_bytes`);generated |
| 219 | + `ConfigFileMount(...)` 构造器对这种 case 已经够简单。 |
| 220 | + |
| 221 | +## 文件级变更汇总 |
| 222 | + |
| 223 | +| 文件 | 变更 | |
| 224 | +| --- | --- | |
| 225 | +| `requirements.txt` | bump `platform-api-python-client` `4.9.0` → `4.10.0` | |
| 226 | +| `centml/sdk/utils/config_file.py` | 新文件(约 15 行含注释 + import) | |
| 227 | +| `examples/sdk/create_inference.py` | 加 import + `config_file=load_config_file_mount(...)` | |
| 228 | +| `tests/test_sdk_config_file_helper.py` | 新文件,4 个 test(TDD:先写) | |
| 229 | + |
| 230 | +不动任何其他文件。 |
| 231 | + |
| 232 | +## 验证计划 |
| 233 | + |
| 234 | +declare done 之前: |
| 235 | + |
| 236 | +1. `pip install -r requirements.txt` 在新 pin 下成功。 |
| 237 | +2. `python -c "from centml.sdk.utils.config_file import load_config_file_mount; print(load_config_file_mount.__doc__ or 'ok')"` 没有 ImportError。 |
| 238 | +3. `cd tests && pytest --sanity` 通过;新加的 4 个 test 在 run output |
| 239 | + 中可见。 |
| 240 | +4. `./scripts/format.sh --check` 和 `./scripts/lint.sh` 干净。 |
| 241 | +5. `./scripts/typecheck.sh` 干净(`Optional[str]` 已是仓内风格)。 |
| 242 | +6. Manual smoke(可选,dev cluster 在手时):在 dev 跑修改后的 |
| 243 | + `examples/sdk/create_inference.py`,带一个小的本地 `nginx.conf` 文件, |
| 244 | + 确认 `cclient.get_inference(id).config_file` 与发出的内容一致。 |
| 245 | + |
| 246 | +## Commit 计划 |
| 247 | + |
| 248 | +单个 commit,conventional commits 风格,subject ≤72 字符: |
| 249 | + |
| 250 | +``` |
| 251 | +feat(sdk): support inference_v3 config_file mount |
| 252 | +``` |
| 253 | + |
| 254 | +Body:简要引用平台 PR #3656/#3667 和三处改动(bump、helper、example)。 |
| 255 | +带 `Signed-off-by: Honglin Cao <hocao@nvidia.com>`。 |
| 256 | +禁 `git add -A` / `git add .` —— 四个文件单独 `git add`。 |
| 257 | + |
| 258 | +## 引用 |
| 259 | + |
| 260 | +- 平台 PR [#3656](https://github.com/CentML/platform/pull/3656) —— feature |
| 261 | +- 平台 PR [#3667](https://github.com/CentML/platform/pull/3667) —— 校验收紧 |
| 262 | +- 仓内风格先例:`centml/sdk/utils/client_certs.py` |
| 263 | +- Example 先例:`examples/sdk/create_inference.py` |
| 264 | +- PyPI 验证:`platform-api-python-client==4.10.0` wheel 含 |
| 265 | + `models/config_file_mount.py`(2026-05-21 验证) |
0 commit comments