Skip to content

Commit 66e2e48

Browse files
committed
fix(coding): 支持窗口预热调试请求
1 parent 33ee9f1 commit 66e2e48

8 files changed

Lines changed: 134 additions & 2 deletions

File tree

.trellis/spec/infra/coding-plan-window-warmer.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
- From `ai/coding/window-warmer`: `uv run python window_warmer.py --config window-warmer.toml`
1919
- From `ai/coding/window-warmer`: `uv run python window_warmer.py --config window-warmer.toml --print-next`
2020
- From `ai/coding/window-warmer`: `uv run python window_warmer.py --config window-warmer.toml --once --dry-run`
21+
- From `ai/coding/window-warmer`: `uv run python window_warmer.py --config window-warmer.toml --debug-request --plan glm-coding-plan`
2122
- PM2:
2223
- `pm2 start ai/coding/window-warmer/window-warmer.pm2.config.cjs`
2324
- PM2 app name: `coding-window-warmer`
2425
- Python script:
2526
- Entry file: `ai/coding/window-warmer/window_warmer.py`
2627
- Dependency declaration: `ai/coding/window-warmer/pyproject.toml`
2728
- Locked dependencies: `ai/coding/window-warmer/uv.lock`
29+
- SOCKS proxy support: `httpx[socks]` must remain in project dependencies because LiteLLM/OpenAI may route through host proxy environment variables.
2830
- Helper package: `ai/coding/window-warmer/window_warmer_lib/`
2931

3032
### 3. Contracts
@@ -65,6 +67,7 @@
6567
| `api_key_env` configured but missing from env and `env_file` | Warmup is skipped with missing key diagnostic |
6668
| `health_path` configured but direct target health check fails | Warmup is skipped before completion request |
6769
| `--dry-run` or `scheduler.dry_run=true` | Docker/API readiness checks and completion request are skipped |
70+
| `--debug-request --plan <name>` | Sends one real completion request for the named enabled plan and exits |
6871
| LiteLLM SDK completion fails | Failure is logged without prompt/key/body; retry up to `retry_count` |
6972
| Multiple plans share the same base time | Each plan remains in the event queue and is executed independently |
7073

@@ -87,6 +90,7 @@
8790
- Unit tests for multiple plans with simultaneous base time remaining independently executable.
8891
- Config parse tests for multiple `[[plans]]`.
8992
- Logging regression test asserting real warmups log lifecycle checkpoints without exposing prompt text or API key values.
93+
- Debug request test asserting `--debug-request` can target one enabled plan without running all plans.
9094
- SDK call test mocking the local wrapper around `litellm.completion`, asserting:
9195
- `model` keeps the configured provider-prefixed model.
9296
- `api_base` is the direct target URL.

ai/coding/window-warmer/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ cd ai/coding/window-warmer
7979
uv run python window_warmer.py --config window-warmer.toml --once --dry-run
8080
```
8181

82+
立即发送一次真实调试请求:
83+
84+
```bash
85+
cd ai/coding/window-warmer
86+
uv run python window_warmer.py \
87+
--config window-warmer.toml \
88+
--debug-request \
89+
--plan glm-coding-plan
90+
```
91+
8292
## PM2 管理
8393

8494
启动:

ai/coding/window-warmer/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ name = "coding-window-warmer"
33
version = "0.1.0"
44
requires-python = ">=3.11"
55
dependencies = [
6+
"httpx[socks]>=0.27.0",
67
"litellm>=1.81.0",
78
"python-dotenv>=1.0.0",
89
]

ai/coding/window-warmer/tests/test_window_warmer.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,54 @@ def test_read_api_key_falls_back_to_dotenv_file(self) -> None:
341341

342342
self.assertEqual(warmer.read_api_key(config), "sk-from-file")
343343

344+
def test_run_debug_request_uses_named_enabled_plan(self) -> None:
345+
"""调试请求应只执行指定的启用 plan。
346+
347+
Args:
348+
None.
349+
350+
Returns:
351+
无返回值。
352+
"""
353+
scheduler = warmer.SchedulerConfig(True, 60, 120, 1, 30, False)
354+
first = warmer.parse_plan_config(
355+
{
356+
"name": "first",
357+
"model": "openai/first",
358+
"schedule_mode": "fixed_times",
359+
"times": ["08:00"],
360+
},
361+
scheduler,
362+
)
363+
second = warmer.parse_plan_config(
364+
{
365+
"name": "second",
366+
"model": "openai/second",
367+
"schedule_mode": "fixed_times",
368+
"times": ["08:00"],
369+
},
370+
scheduler,
371+
)
372+
config = warmer.AppConfig(
373+
target=warmer.TargetConfig(
374+
name="z-ai",
375+
base_url="https://open.bigmodel.cn/api/coding/paas/v4",
376+
container_name=None,
377+
api_key_env=None,
378+
env_file=None,
379+
health_path=None,
380+
request_timeout_seconds=30,
381+
),
382+
scheduler=scheduler,
383+
plans=(first, second),
384+
)
385+
386+
with patch("window_warmer_lib.runner.warm_plan", return_value=True) as warm_plan:
387+
exit_code = warmer.run_debug_request(config, plan_name="second")
388+
389+
self.assertEqual(exit_code, 0)
390+
warm_plan.assert_called_once_with(config, second, dry_run=False)
391+
344392

345393
if __name__ == "__main__":
346394
unittest.main()

ai/coding/window-warmer/uv.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ai/coding/window-warmer/window_warmer_lib/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .config import load_config, parse_config, parse_plan_config
44
from .models import AppConfig, PlanConfig, SchedulerConfig, TargetConfig, WarmEvent
5-
from .runner import print_next_event, run_once, run_watch, warm_plan
5+
from .runner import print_next_event, run_debug_request, run_once, run_watch, select_plan, warm_plan
66
from .scheduler import (
77
build_warm_event,
88
build_warm_events,
@@ -37,8 +37,10 @@
3737
"read_api_key",
3838
"request_json",
3939
"run_once",
40+
"run_debug_request",
4041
"run_watch",
4142
"select_next_event",
43+
"select_plan",
4244
"send_warm_completion",
4345
"warm_plan",
4446
]

ai/coding/window-warmer/window_warmer_lib/cli.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from .config import load_config
1313
from .constants import DEFAULT_CONFIG_NAME
14-
from .runner import log, print_next_event, run_once, run_watch
14+
from .runner import log, print_next_event, run_debug_request, run_once, run_watch
1515

1616

1717
def default_config_path() -> Path:
@@ -51,6 +51,15 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
5151
action="store_true",
5252
help="Send one warmup request for every enabled plan immediately, then exit.",
5353
)
54+
parser.add_argument(
55+
"--debug-request",
56+
action="store_true",
57+
help="Send one real warmup request for a single plan immediately, then exit.",
58+
)
59+
parser.add_argument(
60+
"--plan",
61+
help="Plan name for --debug-request. Defaults to the first enabled plan.",
62+
)
5463
parser.add_argument(
5564
"--print-next",
5665
action="store_true",
@@ -81,6 +90,8 @@ def main(argv: list[str] | None = None) -> int:
8190
try:
8291
if args.print_next:
8392
return print_next_event(config, rng)
93+
if args.debug_request:
94+
return run_debug_request(config, plan_name=args.plan)
8495
if args.once:
8596
return run_once(config, dry_run=dry_run)
8697
return run_watch(config, rng, dry_run=dry_run)

ai/coding/window-warmer/window_warmer_lib/runner.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@
1212
from .target import ensure_target_ready, send_warm_completion
1313

1414

15+
def select_plan(config: AppConfig, plan_name: str | None) -> PlanConfig | None:
16+
"""选择一个启用的 plan。
17+
18+
Args:
19+
config: 应用配置。
20+
plan_name: 可选 plan 名称;为空时选择第一个启用 plan。
21+
22+
Returns:
23+
找到时返回 plan,否则返回 None。
24+
"""
25+
enabled_plans = [plan for plan in config.plans if plan.enabled]
26+
if plan_name is None:
27+
return enabled_plans[0] if enabled_plans else None
28+
return next((plan for plan in enabled_plans if plan.name == plan_name), None)
29+
30+
1531
def warm_plan(config: AppConfig, plan: PlanConfig, dry_run: bool = False) -> bool:
1632
"""执行单个 plan 的预热请求。
1733
@@ -70,6 +86,30 @@ def warm_plan(config: AppConfig, plan: PlanConfig, dry_run: bool = False) -> boo
7086
return False
7187

7288

89+
def run_debug_request(config: AppConfig, plan_name: str | None = None) -> int:
90+
"""立即发送一次指定 plan 的真实调试请求。
91+
92+
Args:
93+
config: 应用配置。
94+
plan_name: 可选 plan 名称;为空时使用第一个启用 plan。
95+
96+
Returns:
97+
调试请求成功返回 0,否则返回 1。
98+
"""
99+
plan = select_plan(config, plan_name)
100+
if plan is None:
101+
if plan_name is None:
102+
log("debug request failed: 没有启用的 plan。")
103+
else:
104+
log(f"debug request failed: 未找到启用的 plan name={plan_name}")
105+
return 1
106+
107+
log(f"debug request started plan={plan.name}")
108+
success = warm_plan(config, plan, dry_run=False)
109+
log(f"debug request finished plan={plan.name} success={str(success).lower()}")
110+
return 0 if success else 1
111+
112+
73113
def run_once(config: AppConfig, dry_run: bool = False) -> int:
74114
"""立即执行所有启用 plan 的预热。
75115

0 commit comments

Comments
 (0)