peninsula123 commited on
Commit
a1231c7
·
1 Parent(s): 828386c

feat(main): finish data struct process

Browse files
open_cortex/runtime/client.py CHANGED
@@ -17,7 +17,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
17
  request_body = {
18
  "messages": to_llama_messages(message),
19
  "temperature": 0.2,
20
- "max_tokens": 100,
21
  "stream": True,
22
  "stream_options": {"include_usage": True},
23
  "timings_per_token": True,
 
17
  request_body = {
18
  "messages": to_llama_messages(message),
19
  "temperature": 0.2,
20
+ "max_tokens": 1024,
21
  "stream": True,
22
  "stream_options": {"include_usage": True},
23
  "timings_per_token": True,
open_cortex/ui/__init__.py ADDED
File without changes
open_cortex/ui/app.py ADDED
File without changes
open_cortex/ui/gradio_history.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from open_cortex.runtime.messages import ChatMessage
2
+
3
+
4
+ def normalize_gradio_content(content) -> str | None:
5
+ if isinstance(content, str):
6
+ return content
7
+
8
+ if isinstance(content, list):
9
+ parts = []
10
+
11
+ for item in content:
12
+ if isinstance(item, dict) and isinstance(item.get("text"), str):
13
+ parts.append(item["text"])
14
+ elif isinstance(item, str):
15
+ parts.append(item)
16
+
17
+ return "\n".join(parts)
18
+
19
+ if isinstance(content, dict) and isinstance(content.get("text"), str):
20
+ return content["text"]
21
+
22
+ return None
23
+
24
+
25
+ def history_to_chat_messages(history: list) -> list[ChatMessage]:
26
+ messages = []
27
+
28
+ for item in history:
29
+ if isinstance(item, dict):
30
+ role = item.get("role")
31
+ content = normalize_gradio_content(item.get("content"))
32
+
33
+ if role in {"system", "user", "assistant"} and content and content.strip():
34
+ messages.append(ChatMessage(role=role, content=content.strip()))
35
+
36
+ elif isinstance(item, (list, tuple)) and len(item) == 2:
37
+ user_content = normalize_gradio_content(item[0])
38
+ assistant_content = normalize_gradio_content(item[1])
39
+
40
+ if user_content and user_content.strip():
41
+ messages.append(ChatMessage(role="user", content=user_content.strip()))
42
+
43
+ if assistant_content and assistant_content.strip():
44
+ messages.append(ChatMessage(role="assistant", content=assistant_content.strip()))
45
+
46
+ return messages
pyproject.toml CHANGED
@@ -9,10 +9,15 @@ dependencies = [
9
  "httpx>=0.28.1",
10
  ]
11
 
12
- [[tool.uv.index]]
13
- url = "https://pypi.tuna.tsinghua.edu.cn/simple"
14
- default = true
15
-
16
  [build-system]
17
  requires = ["hatchling"]
18
  build-backend = "hatchling.build"
 
 
 
 
 
 
 
 
 
 
9
  "httpx>=0.28.1",
10
  ]
11
 
 
 
 
 
12
  [build-system]
13
  requires = ["hatchling"]
14
  build-backend = "hatchling.build"
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest>=9.1.0",
19
+ ]
20
+
21
+ [[tool.uv.index]]
22
+ url = "https://pypi.tuna.tsinghua.edu.cn/simple"
23
+ default = true
scripts/gradio_chat.py CHANGED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from open_cortex.ui.gradio_history import history_to_chat_messages
3
+ from open_cortex.runtime.client import stream_chat_events
4
+
5
+
6
+ def format_runtime(event) -> str:
7
+ if event.kind == "request_started":
8
+ return "Phase: request started\nWaiting for first token..."
9
+
10
+ if event.kind == "first_token" and event.snapshot is not None:
11
+ snapshot = event.snapshot
12
+ context_tokens = (
13
+ snapshot.slot_context_tokens[0]
14
+ if snapshot.slot_context_tokens
15
+ else None
16
+ )
17
+
18
+ return "\n".join(
19
+ [
20
+ "Phase: first token",
21
+ f"TTFT: {event.ttft_ms:.1f} ms",
22
+ f"Context: {context_tokens} / {snapshot.slot_context_size}",
23
+ f"Token Stream: {snapshot.decode_tps} tok/s",
24
+ (
25
+ "Engine: "
26
+ f"processing={snapshot.requests_processing} "
27
+ f"deferred={snapshot.requests_deferred}"
28
+ ),
29
+ ]
30
+ )
31
+
32
+ if event.kind == "request_completed":
33
+ return "\n".join(
34
+ [
35
+ "Phase: completed",
36
+ f"Prompt tokens: {event.prompt_tokens}",
37
+ f"Output tokens: {event.completion_tokens}",
38
+ f"Prefill: {event.prompt_tps:.1f} tok/s",
39
+ f"Decode: {event.decode_tps:.1f} tok/s",
40
+ ]
41
+ )
42
+
43
+ return "Phase: decoding"
44
+
45
+ def user(user_message: str, history: list[dict]) -> tuple[str, list[dict]]:
46
+ if not user_message.strip():
47
+ return "", history
48
+
49
+ return "", history + [
50
+ {
51
+ "role": "user",
52
+ "content": user_message,
53
+ }
54
+ ]
55
+
56
+ def bot(history: list):
57
+ messages = history_to_chat_messages(history)
58
+
59
+ history.append(
60
+ {
61
+ "role": "assistant",
62
+ "content": "",
63
+ }
64
+ )
65
+
66
+ history.append(
67
+ {
68
+ "role": "assistant",
69
+ "content": "",
70
+ }
71
+ )
72
+
73
+ runtime_text = "Phase: idle"
74
+
75
+ for event in stream_chat_events(messages):
76
+ runtime_text = format_runtime(event)
77
+
78
+ if event.text_delta:
79
+ history[-1]["content"] += event.text_delta
80
+
81
+ yield history, runtime_text
82
+
83
+
84
+ with gr.Blocks() as demo:
85
+ gr.Markdown("# OpenCortex Minimal Chat")
86
+
87
+ with gr.Row():
88
+ with gr.Column(scale=2):
89
+ chatbot = gr.Chatbot(
90
+ height=480,
91
+ )
92
+ msg = gr.Textbox(
93
+ placeholder="Ask the local model...",
94
+ show_label=False,
95
+ )
96
+ clear = gr.Button("Clear")
97
+
98
+ with gr.Column(scale=1):
99
+ runtime = gr.Textbox(
100
+ label="Runtime",
101
+ value="Phase: idle",
102
+ lines=10,
103
+ interactive=False,
104
+ )
105
+
106
+
107
+ msg.submit(
108
+ user,
109
+ [msg, chatbot],
110
+ [msg, chatbot],
111
+ queue=False,
112
+ ).then(
113
+ bot,
114
+ chatbot,
115
+ [chatbot,runtime],
116
+ )
117
+
118
+ clear.click(
119
+ lambda: ([],"Phase: idle"),
120
+ None,
121
+ [chatbot, runtime],
122
+ queue=False,
123
+ )
124
+
125
+ if __name__ == "__main__":
126
+ demo.queue()
127
+ demo.launch()
tests/runtime/test_metrics.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from open_cortex.runtime.metrics import parse_prometheus_value
2
+
3
+
4
+ def test_parse_prometheus_value_reads_single_metric():
5
+ text = """
6
+ # HELP llamacpp:requests_processing Number of requests processing.
7
+ # TYPE llamacpp:requests_processing gauge
8
+ llamacpp:requests_processing 1
9
+ """
10
+
11
+ value = parse_prometheus_value(text, "llamacpp:requests_processing")
12
+
13
+ assert value == 1.0
14
+
15
+
16
+ def test_parse_prometheus_value_returns_none_when_missing():
17
+ text = "llamacpp:requests_processing 1\n"
18
+
19
+ value = parse_prometheus_value(text, "llamacpp:requests_deferred")
20
+
21
+ assert value is None
tests/ui/test_gradio_history.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from open_cortex.ui.gradio_history import history_to_chat_messages
2
+
3
+
4
+ def test_history_to_chat_messages_extracts_text_from_gradio_list_content():
5
+ history = [
6
+ {
7
+ "role": "user",
8
+ "content": [
9
+ {
10
+ "text": "请解释 KV Cache",
11
+ "type": "text",
12
+ }
13
+ ],
14
+ }
15
+ ]
16
+
17
+ messages = history_to_chat_messages(history)
18
+
19
+ assert len(messages) == 1
20
+ assert messages[0].role == "user"
21
+ assert messages[0].content == "请解释 KV Cache"
22
+
23
+
24
+ def test_history_to_chat_messages_ignores_non_text_content():
25
+ history = [
26
+ {
27
+ "role": "user",
28
+ "content": [
29
+ {
30
+ "path": "/tmp/image.png",
31
+ "type": "image",
32
+ }
33
+ ],
34
+ }
35
+ ]
36
+
37
+ messages = history_to_chat_messages(history)
38
+
39
+ assert messages == []
40
+
41
+
42
+ def test_history_to_chat_messages_supports_legacy_pair_format():
43
+ history = [
44
+ [
45
+ "hello",
46
+ "hi",
47
+ ]
48
+ ]
49
+
50
+ messages = history_to_chat_messages(history)
51
+
52
+ assert [(m.role, m.content) for m in messages] == [
53
+ ("user", "hello"),
54
+ ("assistant", "hi"),
55
+ ]
uv.lock CHANGED
@@ -378,6 +378,15 @@ wheels = [
378
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
379
  ]
380
 
 
 
 
 
 
 
 
 
 
381
  [[package]]
382
  name = "jinja2"
383
  version = "3.1.6"
@@ -544,12 +553,20 @@ dependencies = [
544
  { name = "httpx" },
545
  ]
546
 
 
 
 
 
 
547
  [package.metadata]
548
  requires-dist = [
549
  { name = "gradio", specifier = ">=6.18.0" },
550
  { name = "httpx", specifier = ">=0.28.1" },
551
  ]
552
 
 
 
 
553
  [[package]]
554
  name = "orjson"
555
  version = "3.11.9"
@@ -733,6 +750,15 @@ wheels = [
733
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
734
  ]
735
 
 
 
 
 
 
 
 
 
 
736
  [[package]]
737
  name = "pydantic"
738
  version = "2.13.4"
@@ -841,6 +867,22 @@ wheels = [
841
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
842
  ]
843
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  [[package]]
845
  name = "python-dateutil"
846
  version = "2.9.0.post0"
 
378
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
379
  ]
380
 
381
+ [[package]]
382
+ name = "iniconfig"
383
+ version = "2.3.0"
384
+ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
385
+ sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
386
+ wheels = [
387
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
388
+ ]
389
+
390
  [[package]]
391
  name = "jinja2"
392
  version = "3.1.6"
 
553
  { name = "httpx" },
554
  ]
555
 
556
+ [package.dev-dependencies]
557
+ dev = [
558
+ { name = "pytest" },
559
+ ]
560
+
561
  [package.metadata]
562
  requires-dist = [
563
  { name = "gradio", specifier = ">=6.18.0" },
564
  { name = "httpx", specifier = ">=0.28.1" },
565
  ]
566
 
567
+ [package.metadata.requires-dev]
568
+ dev = [{ name = "pytest", specifier = ">=9.1.0" }]
569
+
570
  [[package]]
571
  name = "orjson"
572
  version = "3.11.9"
 
750
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
751
  ]
752
 
753
+ [[package]]
754
+ name = "pluggy"
755
+ version = "1.6.0"
756
+ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
757
+ sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
758
+ wheels = [
759
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
760
+ ]
761
+
762
  [[package]]
763
  name = "pydantic"
764
  version = "2.13.4"
 
867
  { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
868
  ]
869
 
870
+ [[package]]
871
+ name = "pytest"
872
+ version = "9.1.0"
873
+ source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
874
+ dependencies = [
875
+ { name = "colorama", marker = "sys_platform == 'win32'" },
876
+ { name = "iniconfig" },
877
+ { name = "packaging" },
878
+ { name = "pluggy" },
879
+ { name = "pygments" },
880
+ ]
881
+ sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/0e/b5858858d74958632c49b72cb25a3976ff9f632397626715be71c89d3971/pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c", size = 1634181, upload-time = "2026-06-13T18:52:45.983Z" }
882
+ wheels = [
883
+ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/5a/ba30a81239b909821b3153e303e7def45178bf353da4f72380e6c5e8793b/pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32", size = 386453, upload-time = "2026-06-13T18:52:44.045Z" },
884
+ ]
885
+
886
  [[package]]
887
  name = "python-dateutil"
888
  version = "2.9.0.post0"