forked from luopingyi/xc_agent_platform_demo
Compare commits
1 Commits
v0.2.0-pha
...
68f24e6794
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f24e6794 |
@@ -45,5 +45,7 @@
|
|||||||
- `POST /api/adapt/task/build-config`,query 为 `gpuType` / `framework` / `taskType`,空 body;成功 JSON `code=0`,`data` 为 YAML。
|
- `POST /api/adapt/task/build-config`,query 为 `gpuType` / `framework` / `taskType`,空 body;成功 JSON `code=0`,`data` 为 YAML。
|
||||||
- UI 无「自动配置」标签;选择框架触发 effect 填入 configParams。模型地址不进入该请求。
|
- UI 无「自动配置」标签;选择框架触发 effect 填入 configParams。模型地址不进入该请求。
|
||||||
- 抓包观察 Authorization(值未记录);容器默认 Xc-Token 社区实践,可显式切换 Authorization,尚无 token 鉴权实证。
|
- 抓包观察 Authorization(值未记录);容器默认 Xc-Token 社区实践,可显式切换 Authorization,尚无 token 鉴权实证。
|
||||||
- 默认 dry-run 离线 sample;CONFIG_PARAMS_SOURCE=build-config 或 ALLOW_BUILD_CONFIG=1 且 token 可解析时调用 API,失败不可静默回退。
|
- 默认 dry-run:`resolve_token()` 成功时优先 build-config;失败回退 sample,逐条报告 configParams_source、GAP 和 fallback reason。无 token → sample。CONFIG_PARAMS_SOURCE=sample 强制离线;=build-config 强制 API 路径(dry-run with_meta 仍可回退)。make build-config-dry 保持失败非零退出。
|
||||||
|
- API gpuType 仅映射壁砺/壁砺166M/Biren_166m 与曦望/曦望 S2/Sunrise_pt-200-x1;未知卡不请求 API,报告 GAP: gpu enum unmapped。
|
||||||
|
- 选策优化后仍为 experience_order,perf_order=unset;order_source=experience;无正式性能表。Section A 仅按 foothold 文本将 verified_reuse 提前,其余 A 保持文件序,再接 B。API foothold evidence: GAP (not wired)。
|
||||||
- **live 前必须有 token + API 调用实证**;sample 单独不是 production-ready。队列 ≥95 继续禁 live,本轮无 live。
|
- **live 前必须有 token + API 调用实证**;sample 单独不是 production-ready。队列 ≥95 继续禁 live,本轮无 live。
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# Tag 文案草稿(仍默认 dry-run)— 未授权勿 push
|
# Tag 文案草稿(仍默认 dry-run)
|
||||||
|
|
||||||
- 建议 Tag 名(待 Victor 确认):`v0.2.0-phase2-dryrun` 或 `v0.2.0-submit-ready-dryrun`
|
- 建议 Tag:`v0.2.0-phase2-dryrun`
|
||||||
- 相对 Phase1 `v0.1.0-phase1@0d0f3b6`:pending A 池 + 经验序排序、只读 task/page 探针、task/add 客户端仍双重闸门默认关、configParams sample 仅 dry-run
|
- 基线:控制台当前 `v0.1.0-phase1` @ `0d0f3b6`
|
||||||
- **不含**:live 开闸、真实自动配置、开闸环境变量默认开启
|
- 相对 Phase1:pending **Section A** + 经验序;只读 `task/page` 探针;`build-config` 客户端(默认离线 sample,opt-in 才调 API);`task/add` 双重闸门默认关
|
||||||
- 推 Tag 条件:仅当 Victor **明确**说「先上线 dry-run 客户端」;产品当前:**先别 push Tag**
|
- **不含**:live 开闸、默认真实提交
|
||||||
- 队列满 / configParams 缺口期间:**不开闸**
|
- 推送:仅 Victor 本人;Agent 不代 push
|
||||||
|
- 队列≥95 / 无书面批准:**不开闸**
|
||||||
|
|
||||||
推送检查清单(人工):
|
检查清单:
|
||||||
1. `make test` 绿
|
1. `make test` 42/42
|
||||||
2. ALLOW_LIVE_SUBMIT 默认非 1;LIVE_CONFIRM 未设
|
2. `ALLOW_LIVE_SUBMIT` 默认非 1
|
||||||
3. 控制台重建后日志仍为 dry-run / Phase2 格式
|
3. 重建后 `/health` 200;`/status` token/strategy 布尔
|
||||||
4. 不代注册、不代 live
|
4. 不代注册、不代 live
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Read-only Phase2 pending draft report. Offline by default; opt-in configuration generation only."""
|
"""Read-only Phase2 pending draft report. Token-resolvable build-config first; explicit sample fallback."""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -10,7 +10,7 @@ from zoneinfo import ZoneInfo
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(ROOT))
|
sys.path.insert(0, str(ROOT))
|
||||||
from src.pending_pool import build_ordered_candidates, input_path, experience_order
|
from src.pending_pool import build_ordered_candidates, input_path, experience_order
|
||||||
from src.config_params import build_config_params, config_params_source, SOURCE, GAP
|
from src.config_params import build_config_params_with_meta, SOURCE, GAP
|
||||||
from src.queue_depth import queue_depth
|
from src.queue_depth import queue_depth
|
||||||
from src.task_add_client import build_payload
|
from src.task_add_client import build_payload
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ def _render(candidates, ts, src):
|
|||||||
c.get('section', 'B') == os.environ['PENDING_SECTION'].strip().upper()]
|
c.get('section', 'B') == os.environ['PENDING_SECTION'].strip().upper()]
|
||||||
depth = queue_depth()
|
depth = queue_depth()
|
||||||
lines = [f'# 代提候选 / 需 Victor 确认 — Dry-run — {ts} (Asia/Shanghai)', '',
|
lines = [f'# 代提候选 / 需 Victor 确认 — Dry-run — {ts} (Asia/Shanghai)', '',
|
||||||
'> 模式:dry-run;零网络写;需 Victor 人工提交/确认',
|
'> 模式:dry-run;仅配置生成 API;无任务提交;需 Victor 人工提交/确认',
|
||||||
'> 积分:待平台核实', '> perf_order=unset;order_source=experience(经验序 / 待平台核实)',
|
'> 积分:待平台核实', '> perf_order=unset;order_source=experience(经验序 / 待平台核实)',
|
||||||
f'> configParams source: {SOURCE}', f'> configParams_source={config_params_source()}', f'> {GAP}', '',
|
f'> configParams source: {SOURCE}', '> API foothold evidence: GAP (not wired)', f'> {GAP}', '',
|
||||||
'## A→B 顺序草稿', '', f'输入:{src};候选条数:{len(candidates)}',
|
'## A→B 顺序草稿', '', f'输入:{src};候选条数:{len(candidates)}',
|
||||||
f'队列深度:{depth if depth is not None else "未知,需人工确认"}',
|
f'队列深度:{depth if depth is not None else "未知,需人工确认"}',
|
||||||
'队列≥95:禁 live;适配 W1 优先,不抢空位。', '']
|
'队列≥95:禁 live;适配 W1 优先,不抢空位。', '']
|
||||||
@@ -45,8 +45,11 @@ def _render(candidates, ts, src):
|
|||||||
f'过滤:{c.get("skip_reason") or "名称硬门未命中;元数据/唯一性待人工核实"}', ''])
|
f'过滤:{c.get("skip_reason") or "名称硬门未命中;元数据/唯一性待人工核实"}', ''])
|
||||||
lines.extend([f'未适配卡(仅 pending 缺口):{", ".join(gpus) or "未提供,需确认"}',
|
lines.extend([f'未适配卡(仅 pending 缺口):{", ".join(gpus) or "未提供,需确认"}',
|
||||||
'perf_order=unset;order_source=experience;天垓100 一般不扇出', ''])
|
'perf_order=unset;order_source=experience;天垓100 一般不扇出', ''])
|
||||||
|
yaml, meta = build_config_params_with_meta(target)
|
||||||
|
lines.extend([f'foothold_class={c.get("foothold_class", "unknown")}; evidence_source={c.get("evidence_source", "GAP")}; no_download={c.get("no_download", False)}',
|
||||||
|
'; '.join(f'{key}={value}' for key, value in meta.items()), ''])
|
||||||
draft = build_payload(model, target, 'vllm', strategy_id='<STRATEGY_ID>',
|
draft = build_payload(model, target, 'vllm', strategy_id='<STRATEGY_ID>',
|
||||||
config_params=build_config_params(target))
|
config_params=yaml)
|
||||||
# Display only; this truncated draft must never be submitted.
|
# Display only; this truncated draft must never be submitted.
|
||||||
draft['configParams'] = draft['configParams'][:400] + '\n… [truncated; dry-run only]'
|
draft['configParams'] = draft['configParams'][:400] + '\n… [truncated; dry-run only]'
|
||||||
lines.extend(['build-config query: gpuType=<targetGpu>&framework=vllm&taskType=text-generation; POST empty body', 'POST 字段草稿(skipped 行禁止提交):', '```json', json.dumps(draft, ensure_ascii=False, indent=2), '```',
|
lines.extend(['build-config query: gpuType=<targetGpu>&framework=vllm&taskType=text-generation; POST empty body', 'POST 字段草稿(skipped 行禁止提交):', '```json', json.dumps(draft, ensure_ascii=False, indent=2), '```',
|
||||||
|
|||||||
@@ -1,31 +1,66 @@
|
|||||||
"""Offline sample by default; explicit opt-in enables captured build-config API."""
|
"""Dry-run configuration: token-resolvable build-config first; failures fall back to sample with GAP."""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.build_config_client import build_config, BuildConfigError
|
from src.build_config_client import build_config, BuildConfigError
|
||||||
from src.task_add_client import resolve_token
|
from src.task_add_client import resolve_token
|
||||||
|
|
||||||
SOURCE = 'POST /api/adapt/task/build-config (discovered via capture); default templates/config_params.sample.yaml'
|
SOURCE = 'POST /api/adapt/task/build-config (discovered via capture); fallback templates/config_params.sample.yaml'
|
||||||
GAP = 'GAP: build-config API discovered via capture (autoconfig-capture-2026-10-16.md); live/实锤 needs token + API call and auth proof. Sample is dry-run draft only, NOT production-ready.'
|
GAP = 'GAP: build-config API discovered via capture; sample is dry-run draft only, NOT production-ready. API foothold evidence: GAP (not wired).'
|
||||||
|
GPU_ENUMS = {
|
||||||
|
'壁砺': 'Biren_166m', '壁砺166M': 'Biren_166m', 'Biren_166m': 'Biren_166m',
|
||||||
|
'曦望': 'Sunrise_pt-200-x1', '曦望 S2': 'Sunrise_pt-200-x1',
|
||||||
|
'Sunrise_pt-200-x1': 'Sunrise_pt-200-x1',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def config_params_source():
|
def config_params_source():
|
||||||
|
"""Requested source preference for dry-run.
|
||||||
|
|
||||||
|
- CONFIG_PARAMS_SOURCE=sample → force offline sample
|
||||||
|
- CONFIG_PARAMS_SOURCE=build-config → force API path (with soft fallback in with_meta)
|
||||||
|
- default (empty): prefer build-config when resolve_token() succeeds; else sample
|
||||||
|
- ALLOW_BUILD_CONFIG=1 remains a legacy opt-in synonym when source empty and token ok
|
||||||
|
"""
|
||||||
source = os.environ.get('CONFIG_PARAMS_SOURCE', '').strip()
|
source = os.environ.get('CONFIG_PARAMS_SOURCE', '').strip()
|
||||||
if source not in ('', 'sample', 'build-config'):
|
if source not in ('', 'sample', 'build-config'):
|
||||||
raise BuildConfigError('unsupported CONFIG_PARAMS_SOURCE')
|
raise BuildConfigError('unsupported CONFIG_PARAMS_SOURCE')
|
||||||
if source == 'build-config':
|
if source:
|
||||||
return 'build-config'
|
return source
|
||||||
if os.environ.get('ALLOW_BUILD_CONFIG') == '1':
|
try:
|
||||||
|
resolve_token()
|
||||||
|
except (ValueError, OSError):
|
||||||
|
return 'sample'
|
||||||
|
# Token resolvable → prefer build-config (kickoff select-opt default).
|
||||||
|
return 'build-config'
|
||||||
|
|
||||||
|
|
||||||
|
def build_config_params_with_meta(gpu_type=None, framework='vllm', task_type='text-generation'):
|
||||||
|
"""Return (YAML, metadata); API failures explicitly fall back for dry-run.
|
||||||
|
|
||||||
|
scripts/build_config_dry.py calls build_config directly and remains strict.
|
||||||
|
"""
|
||||||
|
source = config_params_source()
|
||||||
|
meta = {'configParams_source': 'sample'}
|
||||||
|
try:
|
||||||
|
resolve_token()
|
||||||
|
except (ValueError, OSError):
|
||||||
|
meta['auth'] = 'missing'
|
||||||
|
else:
|
||||||
|
meta['auth'] = 'ok'
|
||||||
|
gpu_enum = GPU_ENUMS.get(gpu_type)
|
||||||
|
if source == 'build-config' and gpu_enum is None:
|
||||||
|
meta['configParams_fallback_reason'] = 'GAP: gpu enum unmapped'
|
||||||
|
elif source == 'build-config':
|
||||||
try:
|
try:
|
||||||
resolve_token()
|
yaml = build_config(gpu_enum, framework, task_type)
|
||||||
except (ValueError, OSError):
|
except BuildConfigError as error:
|
||||||
pass
|
meta['configParams_fallback_reason'] = 'GAP: ' + error.error_attribution
|
||||||
else:
|
else:
|
||||||
return 'build-config'
|
meta['configParams_source'] = 'build-config'
|
||||||
return 'sample'
|
return yaml, meta
|
||||||
|
return (Path(__file__).resolve().parents[1] / 'templates/config_params.sample.yaml').read_text(), meta
|
||||||
|
|
||||||
|
|
||||||
def build_config_params(gpu_type=None, framework='vllm', task_type='text-generation'):
|
def build_config_params(gpu_type=None, framework='vllm', task_type='text-generation'):
|
||||||
"""Return YAML; opted-in API failures propagate, never fall back to sample."""
|
"""Compatible YAML-only dry-run wrapper, including explicit fallback."""
|
||||||
if config_params_source() == 'build-config':
|
return build_config_params_with_meta(gpu_type, framework, task_type)[0]
|
||||||
return build_config(gpu_type, framework, task_type)
|
|
||||||
return (Path(__file__).resolve().parents[1] / 'templates/config_params.sample.yaml').read_text()
|
|
||||||
|
|||||||
@@ -42,8 +42,31 @@ def experience_order(gpus):
|
|||||||
return sorted(dict.fromkeys(gpus), key=tier)
|
return sorted(dict.fromkeys(gpus), key=tier)
|
||||||
|
|
||||||
|
|
||||||
def _candidate(model, section, gpu_text, notes):
|
def classify_foothold(text):
|
||||||
|
"""Classify literal pending evidence; mixed markers prefer verified."""
|
||||||
|
for label, markers in (
|
||||||
|
('verified_reuse', ('已验证成功', '验证通过')),
|
||||||
|
('in_flight', ('正在验证', '验证排队中', '验证进行中')),
|
||||||
|
('failed_foothold', ('验证失败',)),
|
||||||
|
):
|
||||||
|
if any(marker in text for marker in markers):
|
||||||
|
return label
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def has_no_download_marker(text):
|
||||||
|
"""True only for explicit 免下载/已下载; never inferred from verified alone."""
|
||||||
|
return any(marker in text for marker in ('免下载', '已下载'))
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate(model, section, gpu_text, notes, foothold_text=''):
|
||||||
|
evidence_text = foothold_text if section == 'A' else ''
|
||||||
|
foothold_class = classify_foothold(evidence_text) if section == 'A' else 'unknown'
|
||||||
|
no_download = has_no_download_marker(evidence_text + ' ' + notes)
|
||||||
|
evidence = 'pending-foothold' if (foothold_class != 'unknown' or no_download) else 'GAP'
|
||||||
return dict(model_address=model, section=section,
|
return dict(model_address=model, section=section,
|
||||||
|
foothold_text=foothold_text, foothold_class=foothold_class,
|
||||||
|
evidence_source=evidence, no_download=no_download,
|
||||||
try_next_gpus=experience_order(re.findall(GPUS, gpu_text)),
|
try_next_gpus=experience_order(re.findall(GPUS, gpu_text)),
|
||||||
perf_order="unset", order_source="experience",
|
perf_order="unset", order_source="experience",
|
||||||
notes=notes, comment=notes, skip_reason=None,
|
notes=notes, comment=notes, skip_reason=None,
|
||||||
@@ -62,7 +85,7 @@ def parse_pending_md(path):
|
|||||||
cols = [s.strip() for s in line.strip('|').split('|')]
|
cols = [s.strip() for s in line.strip('|').split('|')]
|
||||||
model = re.search(r'`([^`]+)`', cols[1])
|
model = re.search(r'`([^`]+)`', cols[1])
|
||||||
if model:
|
if model:
|
||||||
rows.append(_candidate(model[1], section, cols[3] if section == 'A' else cols[-1], cols[-1]))
|
rows.append(_candidate(model[1], section, cols[3] if section == 'A' else cols[-1], cols[-1], cols[2] if section == 'A' else ''))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
@@ -76,7 +99,7 @@ def parse_pending_txt(path):
|
|||||||
match = re.match(r'#([AB])', comment)
|
match = re.match(r'#([AB])', comment)
|
||||||
section = match[1] if match else 'B'
|
section = match[1] if match else 'B'
|
||||||
gpu_text = re.split(r'(?:still\s+)?try\s*:', comment, maxsplit=1)[-1] if 'try:' in comment else (comment if section == 'B' else '')
|
gpu_text = re.split(r'(?:still\s+)?try\s*:', comment, maxsplit=1)[-1] if 'try:' in comment else (comment if section == 'B' else '')
|
||||||
row = _candidate(parts[0].strip(), section, gpu_text, comment)
|
row = _candidate(parts[0].strip(), section, gpu_text, comment, comment if section == 'A' else '')
|
||||||
if not match:
|
if not match:
|
||||||
row.update(needs_human=True, status='needs_human')
|
row.update(needs_human=True, status='needs_human')
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@@ -103,4 +126,4 @@ def build_ordered_candidates(md_path=None, txt_path=None, dnr_path=None, banned_
|
|||||||
# Metadata gates (weights, context, riskTag, uniqueness) remain unverified.
|
# Metadata gates (weights, context, riskTag, uniqueness) remain unverified.
|
||||||
row['needs_human'] = not bool(reason)
|
row['needs_human'] = not bool(reason)
|
||||||
row['status'] = 'skipped' if reason else 'needs_human'
|
row['status'] = 'skipped' if reason else 'needs_human'
|
||||||
return sorted(rows, key=lambda r: r['section'] != 'A')
|
return sorted(rows, key=lambda r: (r['section'] != 'A', not (r['foothold_class'] == 'verified_reuse' or r.get('no_download'))))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import unittest
|
|||||||
import os
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from src.config_params import build_config_params, config_params_source, GAP
|
from src.config_params import build_config_params, config_params_source, build_config_params_with_meta, GAP
|
||||||
from src.build_config_client import BuildConfigError
|
from src.build_config_client import BuildConfigError
|
||||||
from scripts.dry_run import _render
|
from scripts.dry_run import _render
|
||||||
|
|
||||||
@@ -16,32 +16,68 @@ class TestConfigParams(unittest.TestCase):
|
|||||||
self.addCleanup(env.stop)
|
self.addCleanup(env.stop)
|
||||||
|
|
||||||
def test_source_selection_and_failure(self):
|
def test_source_selection_and_failure(self):
|
||||||
with patch("urllib.request.urlopen") as network:
|
# No token → sample, no network
|
||||||
self.assertEqual(config_params_source(), "sample")
|
with patch('src.config_params.resolve_token', side_effect=ValueError), patch('src.config_params.build_config') as client:
|
||||||
build_config_params()
|
self.assertEqual(config_params_source(), 'sample')
|
||||||
network.assert_not_called()
|
yaml, meta = build_config_params_with_meta('壁砺')
|
||||||
self.assertIn("discovered via capture", GAP)
|
self.assertEqual(meta['auth'], 'missing')
|
||||||
with patch.dict(os.environ, {"CONFIG_PARAMS_SOURCE": "build-config"}):
|
self.assertEqual(meta['configParams_source'], 'sample')
|
||||||
with self.assertRaisesRegex(BuildConfigError, "missing/invalid credential"):
|
client.assert_not_called()
|
||||||
build_config_params("gpu")
|
# Token alone → prefer build-config (kickoff default)
|
||||||
with patch.dict(os.environ, {"ALLOW_BUILD_CONFIG": "1", "XC_TOKEN": "test-secret"}), patch("src.config_params.build_config", return_value="api: yaml") as client:
|
for gpu, enum in [('壁砺', 'Biren_166m'), ('壁砺166M', 'Biren_166m'),
|
||||||
self.assertEqual(config_params_source(), "build-config")
|
('Biren_166m', 'Biren_166m'), ('曦望', 'Sunrise_pt-200-x1'),
|
||||||
self.assertEqual(build_config_params("gpu"), "api: yaml")
|
('曦望 S2', 'Sunrise_pt-200-x1'), ('Sunrise_pt-200-x1', 'Sunrise_pt-200-x1')]:
|
||||||
client.assert_called_once_with("gpu", "vllm", "text-generation")
|
with self.subTest(gpu=gpu), patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config', return_value='api: yaml') as client:
|
||||||
with patch.dict(os.environ, {"CONFIG_PARAMS_SOURCE": "build-config"}), patch("src.config_params.build_config", side_effect=BuildConfigError("transport/timeout")):
|
self.assertEqual(config_params_source(), 'build-config')
|
||||||
with self.assertRaisesRegex(BuildConfigError, "transport/timeout"):
|
yaml, meta = build_config_params_with_meta(gpu)
|
||||||
build_config_params("gpu")
|
self.assertEqual(yaml, 'api: yaml')
|
||||||
|
self.assertEqual(meta['configParams_source'], 'build-config')
|
||||||
|
self.assertEqual(meta['auth'], 'ok')
|
||||||
|
client.assert_called_once_with(enum, 'vllm', 'text-generation')
|
||||||
|
# Failure → sample + GAP reason
|
||||||
|
with patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config', side_effect=BuildConfigError('transport/timeout')):
|
||||||
|
yaml, meta = build_config_params_with_meta('壁砺')
|
||||||
|
self.assertEqual(meta['configParams_source'], 'sample')
|
||||||
|
self.assertEqual(meta['configParams_fallback_reason'], 'GAP: transport/timeout')
|
||||||
|
self.assertIn('max_model_len: 4096', yaml)
|
||||||
|
# Forced build-config also soft-falls back in with_meta
|
||||||
|
with patch.dict(os.environ, {'CONFIG_PARAMS_SOURCE': 'build-config'}), patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config', side_effect=BuildConfigError('transport/timeout')):
|
||||||
|
yaml, meta = build_config_params_with_meta('壁砺')
|
||||||
|
self.assertEqual(meta['configParams_fallback_reason'], 'GAP: transport/timeout')
|
||||||
|
# Unmapped GPU → no API call
|
||||||
|
for gpu in ('天垓150', '智铠 mrv-100', 'MetaX_c-500', '<GPU>', None):
|
||||||
|
with patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config') as client:
|
||||||
|
_, meta = build_config_params_with_meta(gpu)
|
||||||
|
self.assertIn('GAP: gpu enum unmapped', meta['configParams_fallback_reason'])
|
||||||
|
client.assert_not_called()
|
||||||
|
# Force sample even with token
|
||||||
|
with patch.dict(os.environ, {'CONFIG_PARAMS_SOURCE': 'sample'}), patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config') as client:
|
||||||
|
self.assertEqual(config_params_source(), 'sample')
|
||||||
|
build_config_params('壁砺')
|
||||||
|
client.assert_not_called()
|
||||||
|
|
||||||
|
def test_report_api_failure_metadata(self):
|
||||||
|
with patch('src.config_params.resolve_token', return_value='synthetic'), patch('src.config_params.build_config', side_effect=BuildConfigError('transport/timeout')):
|
||||||
|
report = _render([{'model_address': 'org/a', 'try_next_gpus': ['壁砺'],
|
||||||
|
'foothold_class': 'verified_reuse', 'evidence_source': 'pending-foothold',
|
||||||
|
'no_download': False}], 'test', 'fixture')
|
||||||
|
for value in ('foothold_class=verified_reuse', 'evidence_source=pending-foothold', 'no_download=False',
|
||||||
|
'configParams_source=sample', 'configParams_fallback_reason=GAP: transport/timeout'):
|
||||||
|
self.assertIn(value, report)
|
||||||
|
|
||||||
def test_official_sample_exact(self):
|
def test_official_sample_exact(self):
|
||||||
scrape = Path('/workspace/modelhub-adapt/logs/api-docs-scrape-2026-10-16.md')
|
scrape = Path('/workspace/modelhub-adapt/logs/api-docs-scrape-2026-10-16.md')
|
||||||
if scrape.is_file():
|
if scrape.is_file():
|
||||||
body = json.loads(re.search(r'```json\n(.*?)\n```', scrape.read_text(), re.S)[1])
|
body = json.loads(re.search(r'```json\n(.*?)\n```', scrape.read_text(), re.S)[1])
|
||||||
self.assertEqual(build_config_params(), body['configParams'])
|
with patch('src.config_params.resolve_token', side_effect=ValueError):
|
||||||
self.assertIn('max_model_len: 4096', build_config_params())
|
self.assertEqual(build_config_params(), body['configParams'])
|
||||||
self.assertIn('framework: vllm', build_config_params())
|
with patch('src.config_params.resolve_token', side_effect=ValueError):
|
||||||
|
self.assertIn('max_model_len: 4096', build_config_params())
|
||||||
|
self.assertIn('framework: vllm', build_config_params())
|
||||||
self.assertIn('NOT production-ready', GAP)
|
self.assertIn('NOT production-ready', GAP)
|
||||||
|
|
||||||
def test_report_draft_and_gap(self):
|
def test_report_draft_and_gap(self):
|
||||||
report = _render([{'model_address': 'org/a', 'try_next_gpus': ['壁砺']}], 'test', 'fixture')
|
with patch('src.config_params.resolve_token', side_effect=ValueError):
|
||||||
for value in ('代提候选 / 需 Victor 确认', 'configParams source', 'configParams_source=sample', 'GAP:', 'truncated', 'max_model_len: 4096', 'perf_order=unset', 'order_source=experience'):
|
report = _render([{'model_address': 'org/a', 'try_next_gpus': ['壁砺']}], 'test', 'fixture')
|
||||||
|
for value in ('代提候选 / 需 Victor 确认', 'configParams source', 'configParams_source=sample', 'foothold_class=unknown', 'evidence_source=GAP', 'no_download=False', 'GAP:', 'truncated', 'max_model_len: 4096', 'perf_order=unset', 'order_source=experience'):
|
||||||
self.assertIn(value, report)
|
self.assertIn(value, report)
|
||||||
|
|||||||
@@ -47,3 +47,40 @@ class TestPendingPool(unittest.TestCase):
|
|||||||
self.assertEqual(len(rows), 1)
|
self.assertEqual(len(rows), 1)
|
||||||
self.assertEqual(rows[0]['try_next_gpus'], ['天垓150'])
|
self.assertEqual(rows[0]['try_next_gpus'], ['天垓150'])
|
||||||
self.assertEqual(rows[0]['perf_order'], 'unset')
|
self.assertEqual(rows[0]['perf_order'], 'unset')
|
||||||
|
|
||||||
|
def test_foothold_evidence_and_stable_verified_priority(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
md = root / 'pending.md'
|
||||||
|
md.write_text('## Section B\n| 1 | `org/b` | 0 | 壁砺 |\n## Section A\n| 1 | `org/unknown` | 曦望 | 壁砺 | 已验证成功 |\n| 2 | `org/flight` | 验证失败; 正在验证 | 壁砺 | — |\n| 3 | `org/verified` | 他人:已验证成功; 正在验证 | 壁砺 | — |\n| 4 | `org/fail` | 验证失败 | 曦望 | — |\n| 5 | `org/pass` | 既有:验证通过 | 曦望 | — |\n| 6 | `org/queue` | 验证排队中 | 壁砺 | — |\n| 7 | `org/progress` | 验证进行中 | 壁砺 | — |\n')
|
||||||
|
rows = build_ordered_candidates(md, dnr_path=root/'none', banned_path=root/'none')
|
||||||
|
self.assertEqual([r['model_address'] for r in rows],
|
||||||
|
['org/verified', 'org/pass', 'org/unknown', 'org/flight',
|
||||||
|
'org/fail', 'org/queue', 'org/progress', 'org/b'])
|
||||||
|
self.assertEqual([r['foothold_class'] for r in rows],
|
||||||
|
['verified_reuse', 'verified_reuse', 'unknown', 'in_flight',
|
||||||
|
'failed_foothold', 'in_flight', 'in_flight', 'unknown'])
|
||||||
|
for row in rows:
|
||||||
|
self.assertEqual(row['evidence_source'], 'GAP' if row['foothold_class'] == 'unknown' else 'pending-foothold')
|
||||||
|
self.assertEqual(rows[0]['foothold_text'], '他人:已验证成功; 正在验证')
|
||||||
|
txt = root / 'pending.txt'
|
||||||
|
txt.write_text('org/a\t#A done:曦望; try:壁砺\norg/v\t#A 验证通过; try:壁砺\n')
|
||||||
|
parsed = parse_pending_txt(txt)
|
||||||
|
self.assertEqual(parsed[0]['foothold_class'], 'unknown')
|
||||||
|
self.assertEqual(parsed[0]['evidence_source'], 'GAP')
|
||||||
|
self.assertEqual(parsed[1]['foothold_class'], 'verified_reuse')
|
||||||
|
|
||||||
|
def test_no_download_marker_without_inventing(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
md = root / 'pending.md'
|
||||||
|
md.write_text('## Section A\n| 1 | `org/plain` | 曦望:验证排队中 | 壁砺 | — |\n| 2 | `org/cached` | 壁砺:已下载; 曦望:免下载 | 天垓150 | — |\n| 3 | `org/verified` | 壁砺:已验证成功 | 天垓150 | — |\n')
|
||||||
|
rows = build_ordered_candidates(md, dnr_path=root/'none', banned_path=root/'none')
|
||||||
|
by = {r['model_address']: r for r in rows}
|
||||||
|
self.assertFalse(by['org/plain']['no_download'])
|
||||||
|
self.assertEqual(by['org/plain']['evidence_source'], 'pending-foothold')
|
||||||
|
self.assertTrue(by['org/cached']['no_download'])
|
||||||
|
self.assertEqual(by['org/cached']['evidence_source'], 'pending-foothold')
|
||||||
|
self.assertFalse(by['org/verified']['no_download']) # verified alone does not invent no_download
|
||||||
|
self.assertEqual([r['model_address'] for r in rows],
|
||||||
|
['org/cached', 'org/verified', 'org/plain'])
|
||||||
|
|||||||
Reference in New Issue
Block a user