create-workframe 0.1.10 → 0.1.12
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.
- package/README.md +1 -1
- package/docs/security.md +85 -0
- package/package.json +1 -1
- package/scripts/apply-update-workframe.sh +7 -6
- package/scripts/sync-canonical-to-package.mjs +8 -0
- package/scripts/verify-public-deploy.sh +59 -3
- package/workframe-api/openrouter_catalog.py +4 -4
- package/workframe-api/package.json +2 -2
- package/workframe-api/profile_config_yaml.py +126 -0
- package/workframe-api/route_registry.py +330 -0
- package/workframe-api/run-typecheck.mjs +25 -0
- package/workframe-api/server.py +1523 -1392
- package/workframe-api/test_model_surface_consistency.py +116 -0
- package/workframe-api/test_profile_model_yaml.py +99 -0
- package/workframe-api/test_public_routes.py +41 -0
- package/workframe-api/test_route_registry.py +109 -0
- package/workframe-api/time-bind-steps.py +75 -0
- package/workframe-api/time-perf.py +81 -0
- package/workframe-api/time-warm.py +107 -0
- package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-B0OFRGmJ.js} +1 -1
- package/workframe-ui/public/assets/architecture-7EHR7CIX-Dwwi9yA0.js +1 -0
- package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DeBNltHS.js} +1 -1
- package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-BDhCHu7I.js} +1 -1
- package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-olcDYvtI.js} +1 -1
- package/workframe-ui/public/assets/channel-fNHbDpV4.js +1 -0
- package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-D0obCBn_.js} +1 -1
- package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DX93f-ZZ.js} +1 -1
- package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-BX9B5wUs.js} +1 -1
- package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-C4DGc6RO.js} +1 -1
- package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-Crlja9Lf.js} +1 -1
- package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-JYSm3szI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-_KslxVEl.js} +1 -1
- package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-CpTO-jU8.js} +1 -1
- package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-JGUkCmbI.js} +1 -1
- package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-BscBwGaC.js} +1 -1
- package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-5ZTZ0bpX.js} +1 -1
- package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-DxvitYbW.js} +1 -1
- package/workframe-ui/public/assets/{chunk-LZXEDZCA-CsRdBqUB.js → chunk-LZXEDZCA-DMfdMUwz.js} +2 -2
- package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-Cayl0iV9.js} +1 -1
- package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-_7dyBR5G.js} +1 -1
- package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O--xTpD0yi.js} +1 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-ewTgEQK8.js +1 -0
- package/workframe-ui/public/assets/chunk-WU5MYG2G-D4Tg-A0l.js +1 -0
- package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-CwxJQaeK.js} +1 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CFAgBpEl.js +1 -0
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CFAgBpEl.js +1 -0
- package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-CCspENYv.js} +1 -1
- package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Dv8V6R60.js} +1 -1
- package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-Da48hFPT.js} +1 -1
- package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-dTihc7-3.js} +1 -1
- package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-D_Jqu699.js} +1 -1
- package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-Bt8PtEaZ.js} +1 -1
- package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-BtmPjlh0.js} +1 -1
- package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-Cz2turIT.js} +1 -1
- package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-BxMGCflX.js} +1 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-BIJTP5k-.js +1 -0
- package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-ivyroIZt.js} +1 -1
- package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-B9llwShx.js} +1 -1
- package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-BFQHk4bw.js} +1 -1
- package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-ursegLbc.js} +1 -1
- package/workframe-ui/public/assets/index-8CuZDEIG.css +1 -0
- package/workframe-ui/public/assets/index-xS9lFekI.js +129 -0
- package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-DQpifAsB.js} +1 -1
- package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-C16XP2Q7.js} +1 -1
- package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-CiJoz7rP.js} +1 -1
- package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-CVwEvvUL.js} +1 -1
- package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-CWQ6hX5i.js} +1 -1
- package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-CgQsmU35.js} +1 -1
- package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-Bxbc77dL.js} +1 -1
- package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-C_jMeF0a.js} +2 -2
- package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-4RKV9MZP.js} +3 -3
- package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-DZ7y7Sz0.js} +1 -1
- package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-B9h2I7Vq.js} +1 -1
- package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-B2mP5uHm.js} +1 -1
- package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-DyYKyKiP.js} +1 -1
- package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-CL_6nej-.js} +1 -1
- package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-CNKNDQ7S.js} +1 -1
- package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-eWU_mhFa.js} +1 -1
- package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-DrjcXvEQ.js} +1 -1
- package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-BHWR1xTJ.js} +1 -1
- package/workframe-ui/public/assets/{src-CY_By-r7.js → src-DfJB8kV1.js} +1 -1
- package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-BGJbSd3I.js} +1 -1
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CMLuNyeC.js +1 -0
- package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-DkwoCjiZ.js} +1 -1
- package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-GxfhiqoW.js} +1 -1
- package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-DxrOsars.js} +1 -1
- package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-BsWhpUfr.js} +1 -1
- package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-CrFZWMHS.js} +1 -1
- package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DV8Xpf5I.js} +1 -1
- package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-DqMqnwdZ.js} +1 -1
- package/workframe-ui/public/index.html +4 -5
- package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
- package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
- package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
- package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
- package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
- package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
- package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
- package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
- package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for model save/provider surface (WF-034).
|
|
3
|
+
|
|
4
|
+
Run: python services/workframe-api/test_model_surface_consistency.py
|
|
5
|
+
"""
|
|
6
|
+
import importlib.util
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
ROOT = Path(__file__).resolve().parent
|
|
13
|
+
tmpdir = tempfile.mkdtemp(prefix="wf-model-surface-")
|
|
14
|
+
os.environ["WORKFRAME_AGENTS_DIR"] = tmpdir
|
|
15
|
+
os.environ["WORKFRAME_PROJECT"] = "ModelSurfaceTest"
|
|
16
|
+
os.environ["NATIVE_PROFILE"] = "surface-test-agent"
|
|
17
|
+
|
|
18
|
+
spec = importlib.util.spec_from_file_location("wf_server_model_surface", ROOT / "server.py")
|
|
19
|
+
server = importlib.util.module_from_spec(spec)
|
|
20
|
+
sys.modules["wf_server_model_surface"] = server
|
|
21
|
+
spec.loader.exec_module(server)
|
|
22
|
+
|
|
23
|
+
PROF = "surface-test-agent"
|
|
24
|
+
prof_dir = server._profile_dir(PROF)
|
|
25
|
+
prof_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
(prof_dir / "config.yaml").write_text(
|
|
27
|
+
"model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
)
|
|
30
|
+
cfg_path = prof_dir / "config.yaml"
|
|
31
|
+
before = cfg_path.read_bytes()
|
|
32
|
+
|
|
33
|
+
# A: GET models must not mutate config.yaml
|
|
34
|
+
server.hermes_models(PROF, user_id="", workspace_id="")
|
|
35
|
+
assert cfg_path.read_bytes() == before, "hermes_models must not rewrite config on read"
|
|
36
|
+
|
|
37
|
+
# B: reconcile idempotent when routing already matches codex oauth
|
|
38
|
+
assert server._profile_routing_matches_billing(PROF, "codex")
|
|
39
|
+
assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is False
|
|
40
|
+
|
|
41
|
+
# C: billing resolution honors prefer= when model exists in that catalog
|
|
42
|
+
connected = {"codex", "openrouter"}
|
|
43
|
+
assert server._resolve_billing_provider_for_model(
|
|
44
|
+
"gpt-5.4-medium", connected, prefer="codex",
|
|
45
|
+
) == "codex"
|
|
46
|
+
assert server._resolve_billing_provider_for_model(
|
|
47
|
+
"openrouter/owl-alpha", connected, prefer="openrouter",
|
|
48
|
+
) == "openrouter"
|
|
49
|
+
assert server._resolve_billing_provider_for_model(
|
|
50
|
+
"openai-codex/gpt-5.4-mini", {"codex", "openrouter"},
|
|
51
|
+
) == "codex"
|
|
52
|
+
|
|
53
|
+
# D: set with explicit billing_provider returns billing id in response
|
|
54
|
+
applied = server.hermes_model_set(
|
|
55
|
+
PROF, "gpt-5.4-medium", user_id="", workspace_id="", billing_provider="codex",
|
|
56
|
+
)
|
|
57
|
+
assert applied.get("ok"), applied
|
|
58
|
+
assert applied.get("billing_provider") == "codex"
|
|
59
|
+
assert applied.get("provider") == "codex"
|
|
60
|
+
|
|
61
|
+
# E: reconcile picks model billing over stale openrouter proxy yaml
|
|
62
|
+
(prof_dir / "config.yaml").write_text(
|
|
63
|
+
"model:\n default: openai-codex/gpt-5.4-mini\n provider: custom\n"
|
|
64
|
+
" base_url: http://workframe-api:8080/internal/llm/openrouter/v1\n",
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
)
|
|
67
|
+
_orig_picker = server._user_llm_providers_for_picker
|
|
68
|
+
_orig_can = server._user_can_use_llm
|
|
69
|
+
_orig_sync = server._sync_oauth_llm_to_profile
|
|
70
|
+
server._user_llm_providers_for_picker = lambda uid: {"codex"} if uid == "user-1" else set()
|
|
71
|
+
server._user_can_use_llm = lambda uid, ws, prov: uid == "user-1" and prov == "codex"
|
|
72
|
+
server._sync_oauth_llm_to_profile = lambda *a, **k: None
|
|
73
|
+
try:
|
|
74
|
+
assert server._reconcile_profile_llm_for_user(PROF, "user-1", "") is True
|
|
75
|
+
block = server._read_model_block(PROF)
|
|
76
|
+
assert block.get("provider") == "openai-codex"
|
|
77
|
+
assert block.get("default") == "gpt-5.4-mini"
|
|
78
|
+
assert not block.get("base_url")
|
|
79
|
+
finally:
|
|
80
|
+
server._user_llm_providers_for_picker = _orig_picker
|
|
81
|
+
server._user_can_use_llm = _orig_can
|
|
82
|
+
server._sync_oauth_llm_to_profile = _orig_sync
|
|
83
|
+
|
|
84
|
+
# F: runtime disk wins over template in _read_model_block
|
|
85
|
+
runtime_prof = "u-test-user-surface-test-agent"
|
|
86
|
+
runtime_dir = server._profile_dir(runtime_prof)
|
|
87
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
(runtime_dir / "config.yaml").write_text(
|
|
89
|
+
"model:\n default: gpt-5.4-mini\n provider: openai-codex\n",
|
|
90
|
+
encoding="utf-8",
|
|
91
|
+
)
|
|
92
|
+
block = server._read_model_block(runtime_prof)
|
|
93
|
+
assert block.get("default") == "gpt-5.4-mini", block
|
|
94
|
+
assert block.get("provider") == "openai-codex", block
|
|
95
|
+
|
|
96
|
+
# G: template model save mirrors to runtime (same model id, not stale template re-read)
|
|
97
|
+
runtime_prof = "u-test-user2-surface-test-agent"
|
|
98
|
+
runtime_dir = server._profile_dir(runtime_prof)
|
|
99
|
+
runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
(runtime_dir / "config.yaml").write_text(
|
|
101
|
+
"model:\n default: gpt-5.4-medium\n provider: openai-codex\n",
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
)
|
|
104
|
+
_orig_restart = server._restart_runtime_profile_gateway
|
|
105
|
+
server._restart_runtime_profile_gateway = lambda *a, **k: None
|
|
106
|
+
try:
|
|
107
|
+
applied = server.hermes_model_set(
|
|
108
|
+
PROF, "gpt-5.4-mini", user_id="test-user2", workspace_id="", billing_provider="codex",
|
|
109
|
+
)
|
|
110
|
+
finally:
|
|
111
|
+
server._restart_runtime_profile_gateway = _orig_restart
|
|
112
|
+
assert applied.get("ok"), applied
|
|
113
|
+
runtime_block = server._parse_model_block_from_disk(runtime_prof)
|
|
114
|
+
assert runtime_block.get("default") == "gpt-5.4-mini", runtime_block
|
|
115
|
+
|
|
116
|
+
print("model surface consistency self-check ok")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Coalesce corrupt duplicate model blocks + profile_config_yaml writer (WF-033)."""
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parent
|
|
12
|
+
os.environ.setdefault("WORKFRAME_AGENTS_DIR", tempfile.mkdtemp(prefix="wf-yaml-test-"))
|
|
13
|
+
os.environ.setdefault("WORKFRAME_PROJECT", "YamlTest")
|
|
14
|
+
os.environ.setdefault("NATIVE_PROFILE", "yaml-test-agent")
|
|
15
|
+
|
|
16
|
+
spec = importlib.util.spec_from_file_location("wf_server_yaml_test", ROOT / "server.py")
|
|
17
|
+
server = importlib.util.module_from_spec(spec)
|
|
18
|
+
sys.modules["wf_server_yaml_test"] = server
|
|
19
|
+
spec.loader.exec_module(server)
|
|
20
|
+
|
|
21
|
+
import profile_config_yaml # noqa: E402
|
|
22
|
+
|
|
23
|
+
BROKEN = """model:
|
|
24
|
+
default: gpt-5.4-medium
|
|
25
|
+
provider: openai-codex
|
|
26
|
+
model:
|
|
27
|
+
default: gpt-5.4-medium
|
|
28
|
+
- provider: openai-codex
|
|
29
|
+
fallback_providers:
|
|
30
|
+
- provider: openai-codex
|
|
31
|
+
model: gpt-5.4-mini
|
|
32
|
+
toolsets:
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
prof = "yaml-test-agent"
|
|
36
|
+
prof_dir = server._profile_dir(prof)
|
|
37
|
+
prof_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
cfg_path = prof_dir / "config.yaml"
|
|
39
|
+
cfg_path.write_text(BROKEN, encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
server._normalize_profile_config_yaml(prof)
|
|
42
|
+
fixed = cfg_path.read_text(encoding="utf-8")
|
|
43
|
+
assert fixed.count("\nmodel:\n") + fixed.count("model:\n") <= 2, fixed
|
|
44
|
+
assert " - provider:" not in fixed.split("fallback_providers:")[0], fixed
|
|
45
|
+
parsed = yaml.safe_load(fixed)
|
|
46
|
+
assert isinstance(parsed.get("model"), dict), parsed
|
|
47
|
+
assert parsed["model"]["default"] == "gpt-5.4-medium"
|
|
48
|
+
assert parsed["model"]["provider"] == "openai-codex"
|
|
49
|
+
assert len(parsed.get("fallback_providers") or []) == 1
|
|
50
|
+
assert "toolsets" in parsed
|
|
51
|
+
|
|
52
|
+
# Round-trip via update_model_surface preserves non-model keys
|
|
53
|
+
round_prof = "yaml-round-trip"
|
|
54
|
+
round_dir = server._profile_dir(round_prof)
|
|
55
|
+
round_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
round_path = round_dir / "config.yaml"
|
|
57
|
+
round_path.write_text(
|
|
58
|
+
"# picker note\nmodel:\n default: old-model\n provider: openrouter\ncron: []\n",
|
|
59
|
+
encoding="utf-8",
|
|
60
|
+
)
|
|
61
|
+
profile_config_yaml.update_model_surface(
|
|
62
|
+
round_path, default="new-model", provider="custom", base_url="http://proxy/v1",
|
|
63
|
+
)
|
|
64
|
+
rt = yaml.safe_load(round_path.read_text(encoding="utf-8"))
|
|
65
|
+
assert rt["model"]["default"] == "new-model"
|
|
66
|
+
assert rt["model"]["provider"] == "custom"
|
|
67
|
+
assert rt["model"]["base_url"] == "http://proxy/v1"
|
|
68
|
+
assert rt.get("cron") == []
|
|
69
|
+
assert "# picker note" not in round_path.read_text(encoding="utf-8") # comments not preserved
|
|
70
|
+
|
|
71
|
+
# Missing file: writer creates parent dirs + config
|
|
72
|
+
missing_prof = "yaml-missing"
|
|
73
|
+
missing_dir = server._profile_dir(missing_prof)
|
|
74
|
+
missing_path = missing_dir / "config.yaml"
|
|
75
|
+
if missing_path.is_file():
|
|
76
|
+
missing_path.unlink()
|
|
77
|
+
profile_config_yaml.update_model_surface(
|
|
78
|
+
missing_path, default="gpt-5.4-mini", provider="openai-codex",
|
|
79
|
+
)
|
|
80
|
+
assert missing_path.is_file()
|
|
81
|
+
loaded = yaml.safe_load(missing_path.read_text(encoding="utf-8"))
|
|
82
|
+
assert loaded["model"]["default"] == "gpt-5.4-mini"
|
|
83
|
+
|
|
84
|
+
# Coalesce flag drops duplicate blocks
|
|
85
|
+
dup_path = round_dir / "dup.yaml"
|
|
86
|
+
dup_path.write_text(BROKEN, encoding="utf-8")
|
|
87
|
+
profile_config_yaml.update_model_surface(
|
|
88
|
+
dup_path,
|
|
89
|
+
default="gpt-5.4-medium",
|
|
90
|
+
provider="openai-codex",
|
|
91
|
+
fallback_chain=[{"provider": "openai-codex", "model": "gpt-5.4-mini"}],
|
|
92
|
+
coalesce=True,
|
|
93
|
+
)
|
|
94
|
+
dup_text = dup_path.read_text(encoding="utf-8")
|
|
95
|
+
assert dup_text.count("\nmodel:\n") + dup_text.count("model:\n") <= 2, dup_text
|
|
96
|
+
dup_parsed = yaml.safe_load(dup_text)
|
|
97
|
+
assert dup_parsed["model"]["default"] == "gpt-5.4-medium"
|
|
98
|
+
|
|
99
|
+
print("profile model yaml coalesce self-check ok")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""ponytail self-check: session-less GET policy (WF-031).
|
|
2
|
+
|
|
3
|
+
Run: python services/workframe-api/test_public_routes.py
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
9
|
+
|
|
10
|
+
import server # noqa: E402
|
|
11
|
+
|
|
12
|
+
ALWAYS = "/api/health"
|
|
13
|
+
DATA = "/api/snapshot"
|
|
14
|
+
META = "/api/meta"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def allowed(path: str, mode: str, dev: bool = False) -> bool:
|
|
18
|
+
return server._sessionless_get_allowed(path, deployment_mode=mode, dev_local_unsafe=dev)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Always-public routes work in every deployment mode.
|
|
22
|
+
assert allowed(META, "public_multi_user")
|
|
23
|
+
assert allowed(ALWAYS, "trusted_team")
|
|
24
|
+
assert allowed("/api/public/branding/logo", "public_multi_user")
|
|
25
|
+
|
|
26
|
+
# Data routes: blocked for multi-user modes without DEV_LOCAL_UNSAFE.
|
|
27
|
+
assert not allowed(DATA, "public_multi_user")
|
|
28
|
+
assert not allowed(DATA, "trusted_team")
|
|
29
|
+
assert not allowed("/api/chat/messages", "public_multi_user")
|
|
30
|
+
assert not allowed("/api/files/list", "trusted_team")
|
|
31
|
+
|
|
32
|
+
# Data routes: allowed for single_user_local or dev unsafe.
|
|
33
|
+
assert allowed(DATA, "single_user_local")
|
|
34
|
+
assert allowed(DATA, "public_multi_user", dev=True)
|
|
35
|
+
assert allowed("/api/board", "single_user_local")
|
|
36
|
+
|
|
37
|
+
# _is_public_get composes static/UI paths.
|
|
38
|
+
assert server._is_public_get("/")
|
|
39
|
+
assert server._is_public_get("/assets/app.js")
|
|
40
|
+
|
|
41
|
+
print("public routes self-check ok")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Route registry audit (WF-037).
|
|
3
|
+
|
|
4
|
+
Run: python services/workframe-api/test_route_registry.py
|
|
5
|
+
"""
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parent
|
|
11
|
+
sys.path.insert(0, str(ROOT))
|
|
12
|
+
|
|
13
|
+
import route_registry # noqa: E402
|
|
14
|
+
|
|
15
|
+
# Auth level coverage
|
|
16
|
+
assert route_registry.resolve_auth_level("GET", "/api/health") == route_registry.AuthLevel.PUBLIC
|
|
17
|
+
assert route_registry.resolve_auth_level("GET", "/api/meta") == route_registry.AuthLevel.PUBLIC
|
|
18
|
+
assert route_registry.resolve_auth_level("GET", "/api/snapshot") == route_registry.AuthLevel.SINGLE_USER_GET
|
|
19
|
+
assert route_registry.resolve_auth_level("POST", "/api/auth/start") == route_registry.AuthLevel.AUTH_FLOW
|
|
20
|
+
assert route_registry.resolve_auth_level("GET", "/api/me") == route_registry.AuthLevel.SESSION
|
|
21
|
+
assert route_registry.resolve_auth_level("GET", "/api/activity/detail") == route_registry.AuthLevel.SESSION
|
|
22
|
+
|
|
23
|
+
# Unknown /api paths require session (deny when anonymous)
|
|
24
|
+
assert route_registry.resolve_auth_level("GET", "/api/not-a-real-handler") == route_registry.AuthLevel.SESSION
|
|
25
|
+
assert not route_registry.authorize_request(
|
|
26
|
+
"GET",
|
|
27
|
+
"/api/not-a-real-handler",
|
|
28
|
+
"",
|
|
29
|
+
deployment_mode="trusted_team",
|
|
30
|
+
dev_local_unsafe=False,
|
|
31
|
+
install_window_open=False,
|
|
32
|
+
validate_session=lambda _s: None,
|
|
33
|
+
attach_user=lambda _u: None,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# ROUTES table: auth tier matches resolve_auth_level
|
|
37
|
+
auth_mismatch: list[str] = []
|
|
38
|
+
for route in route_registry.ROUTES:
|
|
39
|
+
resolved = route_registry.resolve_auth_level(route.method, route.path)
|
|
40
|
+
if resolved != route.auth:
|
|
41
|
+
auth_mismatch.append(f"{route.method} {route.path}: table={route.auth} resolved={resolved}")
|
|
42
|
+
assert not auth_mismatch, f"route auth mismatch: {auth_mismatch}"
|
|
43
|
+
|
|
44
|
+
# ROUTES handlers exist on server.Handler
|
|
45
|
+
server_src = (ROOT / "server.py").read_text(encoding="utf-8")
|
|
46
|
+
missing_handlers: list[str] = []
|
|
47
|
+
for route in route_registry.ROUTES:
|
|
48
|
+
if f"def {route.handler}(" not in server_src:
|
|
49
|
+
missing_handlers.append(f"{route.method} {route.path} -> {route.handler}")
|
|
50
|
+
assert not missing_handlers, f"missing handler methods: {missing_handlers}"
|
|
51
|
+
|
|
52
|
+
# Remaining if-chain GET paths still map to a known auth tier
|
|
53
|
+
handler_paths = set(re.findall(r'if path == "(/api/[^"]+)"', server_src))
|
|
54
|
+
handler_paths |= set(re.findall(r"if path == \'(/api/[^\']+)\'", server_src))
|
|
55
|
+
unmapped: list[str] = []
|
|
56
|
+
for p in sorted(handler_paths):
|
|
57
|
+
level = route_registry.resolve_auth_level("GET", p)
|
|
58
|
+
if level not in (
|
|
59
|
+
route_registry.AuthLevel.PUBLIC,
|
|
60
|
+
route_registry.AuthLevel.SINGLE_USER_GET,
|
|
61
|
+
route_registry.AuthLevel.SESSION,
|
|
62
|
+
route_registry.AuthLevel.INSTALL,
|
|
63
|
+
route_registry.AuthLevel.AUTH_FLOW,
|
|
64
|
+
):
|
|
65
|
+
unmapped.append(p)
|
|
66
|
+
assert not unmapped, f"unmapped auth levels: {unmapped}"
|
|
67
|
+
|
|
68
|
+
# trusted_team: data GETs need session (not anonymous), not a hard deny
|
|
69
|
+
assert not route_registry.authorize_request(
|
|
70
|
+
"GET",
|
|
71
|
+
"/api/agents",
|
|
72
|
+
"",
|
|
73
|
+
deployment_mode="trusted_team",
|
|
74
|
+
dev_local_unsafe=False,
|
|
75
|
+
install_window_open=False,
|
|
76
|
+
validate_session=lambda _s: None,
|
|
77
|
+
attach_user=lambda _u: None,
|
|
78
|
+
)
|
|
79
|
+
assert route_registry.authorize_request(
|
|
80
|
+
"GET",
|
|
81
|
+
"/api/agents",
|
|
82
|
+
"sid-ok",
|
|
83
|
+
deployment_mode="trusted_team",
|
|
84
|
+
dev_local_unsafe=False,
|
|
85
|
+
install_window_open=False,
|
|
86
|
+
validate_session=lambda s: {"user_id": "u1"} if s == "sid-ok" else None,
|
|
87
|
+
attach_user=lambda _u: None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
data_read_get = [r for r in route_registry.ROUTES if r.method == "GET"]
|
|
91
|
+
assert len(data_read_get) == 36
|
|
92
|
+
registry_post = [r for r in route_registry.ROUTES if r.method == "POST"]
|
|
93
|
+
assert len(registry_post) == 25
|
|
94
|
+
|
|
95
|
+
class _PostStub:
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
self.called = False
|
|
98
|
+
self.body: dict | None = None
|
|
99
|
+
|
|
100
|
+
def _route_post_auth_start(self, body: dict) -> None:
|
|
101
|
+
self.called = True
|
|
102
|
+
self.body = body
|
|
103
|
+
|
|
104
|
+
_stub = _PostStub()
|
|
105
|
+
assert route_registry.dispatch_post(_stub, "/api/auth/start", {"email": "a@b.c"})
|
|
106
|
+
assert _stub.called and _stub.body == {"email": "a@b.c"}
|
|
107
|
+
assert not route_registry.dispatch_post(_stub, "/api/not-registered", {})
|
|
108
|
+
|
|
109
|
+
print("route registry self-check ok")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Granular bind + stream lease timings (docker exec)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
import server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ms(t0: float) -> int:
|
|
14
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
conn = sqlite3.connect(str(server._workframe_db_path()))
|
|
19
|
+
conn.row_factory = sqlite3.Row
|
|
20
|
+
room = conn.execute(
|
|
21
|
+
"""
|
|
22
|
+
SELECT id, workspace_id FROM rooms
|
|
23
|
+
WHERE room_type = 'direct' AND deleted_at IS NULL
|
|
24
|
+
ORDER BY updated_at DESC LIMIT 1
|
|
25
|
+
""",
|
|
26
|
+
).fetchone()
|
|
27
|
+
user = conn.execute(
|
|
28
|
+
"SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
|
|
29
|
+
).fetchone()
|
|
30
|
+
conn.close()
|
|
31
|
+
if not room or not user:
|
|
32
|
+
print("no room/user", file=sys.stderr)
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
uid = str(user["id"])
|
|
36
|
+
rid = str(room["id"])
|
|
37
|
+
ws = str(room["workspace_id"])
|
|
38
|
+
prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
|
|
39
|
+
payload = {
|
|
40
|
+
"room_id": rid,
|
|
41
|
+
"workspace_id": ws,
|
|
42
|
+
"source_id": "steps",
|
|
43
|
+
"client_id": "steps",
|
|
44
|
+
"binding_version": 2,
|
|
45
|
+
}
|
|
46
|
+
print(f"profile={prof}")
|
|
47
|
+
|
|
48
|
+
block = server._read_model_block(prof)
|
|
49
|
+
t = time.perf_counter()
|
|
50
|
+
block2 = server._read_model_block(prof)
|
|
51
|
+
print(f"read_model_block_ms={ms(t)} repeat_ms={ms(t)}")
|
|
52
|
+
|
|
53
|
+
t = time.perf_counter()
|
|
54
|
+
server.ensure_profile_api(prof, uid, ws)
|
|
55
|
+
print(f"ensure_api_ms={ms(t)}")
|
|
56
|
+
|
|
57
|
+
provider = server._llm_billing_provider(prof, user_id=uid, workspace_id=ws, block=block)
|
|
58
|
+
run_a, run_b = "steps-a", "steps-b"
|
|
59
|
+
t = time.perf_counter()
|
|
60
|
+
server._apply_turn_credential_lease(prof, uid, ws, provider, run_a)
|
|
61
|
+
print(f"lease_cold_ms={ms(t)} provider={provider}")
|
|
62
|
+
t = time.perf_counter()
|
|
63
|
+
server._apply_turn_credential_lease(prof, uid, ws, provider, run_b)
|
|
64
|
+
print(f"lease_warm_ms={ms(t)}")
|
|
65
|
+
|
|
66
|
+
for i in range(3):
|
|
67
|
+
t = time.perf_counter()
|
|
68
|
+
server.profile_chat_bind("", payload, user_id=uid)
|
|
69
|
+
print(f"bind_{i}_ms={ms(t)}")
|
|
70
|
+
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure model-save, gateway reload, bind, and reconcile latency (docker exec)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import server
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ms(t0: float) -> int:
|
|
13
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> int:
|
|
17
|
+
conn = sqlite3.connect(str(server._workframe_db_path()))
|
|
18
|
+
conn.row_factory = sqlite3.Row
|
|
19
|
+
room = conn.execute(
|
|
20
|
+
"""
|
|
21
|
+
SELECT r.id, r.workspace_id
|
|
22
|
+
FROM rooms r
|
|
23
|
+
WHERE r.room_type = 'direct' AND r.deleted_at IS NULL
|
|
24
|
+
ORDER BY r.updated_at DESC
|
|
25
|
+
LIMIT 1
|
|
26
|
+
""",
|
|
27
|
+
).fetchone()
|
|
28
|
+
user = conn.execute(
|
|
29
|
+
"SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
|
|
30
|
+
).fetchone()
|
|
31
|
+
conn.close()
|
|
32
|
+
if not room or not user:
|
|
33
|
+
print("no room/user", file=sys.stderr)
|
|
34
|
+
return 1
|
|
35
|
+
|
|
36
|
+
user_id = str(user["id"])
|
|
37
|
+
room_id = str(room["id"])
|
|
38
|
+
ws_id = str(room["workspace_id"])
|
|
39
|
+
prof = server._resolve_chat_hermes_profile("mybusiness-agent", user_id, room_id, ws_id)
|
|
40
|
+
print(f"profile={prof}")
|
|
41
|
+
|
|
42
|
+
t = time.perf_counter()
|
|
43
|
+
ok, err = server._apply_model_persisted(
|
|
44
|
+
prof, "codex", "gpt-5.4-mini", user_id, restart_gateway=False,
|
|
45
|
+
)
|
|
46
|
+
print(f"yaml_write_ms={ms(t)} ok={ok} err={err!r}")
|
|
47
|
+
|
|
48
|
+
t = time.perf_counter()
|
|
49
|
+
code, out = server._gateway_exec(prof, ["gateway", "restart"])
|
|
50
|
+
# ponytail: ~7s vs stop+start ~45s on dogfood
|
|
51
|
+
healthy = server._wait_profile_api_healthy(prof, attempts=24, delay=0.25)
|
|
52
|
+
print(f"gateway_restart_ms={ms(t)} code={code} healthy={healthy} out={out[:80]!r}")
|
|
53
|
+
|
|
54
|
+
t = time.perf_counter()
|
|
55
|
+
changed = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
|
|
56
|
+
print(f"reconcile_ms={ms(t)} changed={changed}")
|
|
57
|
+
|
|
58
|
+
t = time.perf_counter()
|
|
59
|
+
server.ensure_profile_api(prof, user_id, ws_id)
|
|
60
|
+
print(f"ensure_api_ms={ms(t)}")
|
|
61
|
+
|
|
62
|
+
payload = {
|
|
63
|
+
"room_id": room_id,
|
|
64
|
+
"workspace_id": ws_id,
|
|
65
|
+
"source_id": "perf",
|
|
66
|
+
"client_id": "perf",
|
|
67
|
+
"binding_version": 2,
|
|
68
|
+
}
|
|
69
|
+
t = time.perf_counter()
|
|
70
|
+
session = server.profile_chat_session("", payload, user_id=user_id)
|
|
71
|
+
print(f"bind_ms={ms(t)} session={str(session.get('session_id') or '')[:24]}")
|
|
72
|
+
|
|
73
|
+
t = time.perf_counter()
|
|
74
|
+
changed2 = server._reconcile_profile_llm_for_user(prof, user_id, ws_id)
|
|
75
|
+
print(f"reconcile_repeat_ms={ms(t)} changed={changed2}")
|
|
76
|
+
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Warm-path timings: model save API return + bind + first stream token."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sqlite3
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
import server
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ms(t0: float) -> int:
|
|
16
|
+
return round((time.perf_counter() - t0) * 1000)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
conn = sqlite3.connect(str(server._workframe_db_path()))
|
|
21
|
+
conn.row_factory = sqlite3.Row
|
|
22
|
+
room = conn.execute(
|
|
23
|
+
"""
|
|
24
|
+
SELECT id, workspace_id FROM rooms
|
|
25
|
+
WHERE room_type = 'direct' AND deleted_at IS NULL
|
|
26
|
+
ORDER BY updated_at DESC LIMIT 1
|
|
27
|
+
""",
|
|
28
|
+
).fetchone()
|
|
29
|
+
user = conn.execute(
|
|
30
|
+
"SELECT id FROM users WHERE status = 'active' ORDER BY created_at LIMIT 1",
|
|
31
|
+
).fetchone()
|
|
32
|
+
conn.close()
|
|
33
|
+
if not room or not user:
|
|
34
|
+
print("no room/user", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
uid = str(user["id"])
|
|
38
|
+
rid = str(room["id"])
|
|
39
|
+
ws = str(room["workspace_id"])
|
|
40
|
+
prof = server._resolve_chat_hermes_profile("mybusiness-agent", uid, rid, ws)
|
|
41
|
+
payload = {
|
|
42
|
+
"room_id": rid,
|
|
43
|
+
"workspace_id": ws,
|
|
44
|
+
"source_id": "warm",
|
|
45
|
+
"client_id": "warm",
|
|
46
|
+
"binding_version": 2,
|
|
47
|
+
}
|
|
48
|
+
print(f"profile={prof} healthy={server._profile_api_healthy(prof)}")
|
|
49
|
+
|
|
50
|
+
t = time.perf_counter()
|
|
51
|
+
result = server.hermes_model_set(
|
|
52
|
+
prof, "gpt-5.4-medium", user_id=uid, workspace_id=ws, billing_provider="codex",
|
|
53
|
+
)
|
|
54
|
+
print(f"model_set_ms={ms(t)} ok={result.get('ok')}")
|
|
55
|
+
|
|
56
|
+
time.sleep(0.5)
|
|
57
|
+
t = time.perf_counter()
|
|
58
|
+
session = server.profile_chat_session("", payload, user_id=uid)
|
|
59
|
+
sid = str(session.get("session_id") or "")
|
|
60
|
+
print(f"bind_ms={ms(t)} session={sid[:28]}")
|
|
61
|
+
|
|
62
|
+
t = time.perf_counter()
|
|
63
|
+
server.profile_chat_session("", payload, user_id=uid)
|
|
64
|
+
print(f"bind_repeat_ms={ms(t)}")
|
|
65
|
+
|
|
66
|
+
for i in range(2):
|
|
67
|
+
t = time.perf_counter()
|
|
68
|
+
server._reconcile_profile_llm_for_user(prof, uid, ws)
|
|
69
|
+
print(f"reconcile_cached_{i}_ms={ms(t)}")
|
|
70
|
+
|
|
71
|
+
t = time.perf_counter()
|
|
72
|
+
server.profile_chat_session("", payload, user_id=uid)
|
|
73
|
+
print(f"bind_cached_ms={ms(t)}")
|
|
74
|
+
|
|
75
|
+
port = server._profile_api_port(prof)
|
|
76
|
+
key = server._profile_api_key(prof)
|
|
77
|
+
body = json.dumps(server._profile_turn_payload(prof, "Reply with exactly: pong", rid)).encode()
|
|
78
|
+
url = f"http://gateway:{port}/api/sessions/{sid}/chat/stream"
|
|
79
|
+
req = urllib.request.Request(
|
|
80
|
+
url,
|
|
81
|
+
data=body,
|
|
82
|
+
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
83
|
+
method="POST",
|
|
84
|
+
)
|
|
85
|
+
t = time.perf_counter()
|
|
86
|
+
first_ms = None
|
|
87
|
+
try:
|
|
88
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
89
|
+
while True:
|
|
90
|
+
chunk = resp.read(4096)
|
|
91
|
+
if not chunk:
|
|
92
|
+
break
|
|
93
|
+
if first_ms is None and b"data:" in chunk:
|
|
94
|
+
first_ms = ms(t)
|
|
95
|
+
print(f"first_upstream_chunk_ms={first_ms} sample={chunk[:120]!r}")
|
|
96
|
+
break
|
|
97
|
+
except urllib.error.HTTPError as exc:
|
|
98
|
+
print(f"stream_http_error={exc.code} body={exc.read()[:200]!r}")
|
|
99
|
+
return 1
|
|
100
|
+
if first_ms is None:
|
|
101
|
+
print("no_stream_chunk")
|
|
102
|
+
return 1
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(main())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-
|
|
1
|
+
import{n as e,t}from"./path-BWPyau1x.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-Cz2turIT.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i)if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./chunk-NNHCCRGN-DlpIbxXb.js";import{x as e}from"./mermaid-parser.core-C_jMeF0a.js";export{e as createArchitectureServices};
|