J.B-Lin commited on
Commit
ff5d4ed
ยท
1 Parent(s): ed20b95

feat: llama.cpp deploys MiniCPM-o 4.5 on Modal - technical report and client API

Browse files
.gitignore CHANGED
@@ -1,2 +1,38 @@
 
1
  .vscode/
2
- *.pyc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # IDE
2
  .vscode/
3
+ .idea/
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+
13
+ # ๆจกๅž‹ๆ–‡ไปถ๏ผˆๅคชๅคง๏ผŒไธ็บณๅ…ฅ็‰ˆๆœฌๆŽงๅˆถ๏ผ‰
14
+ *.gguf
15
+ models/
16
+ llamacpp/
17
+
18
+ # llama.cpp ็ผ–่ฏ‘ไบง็‰ฉ
19
+ llama.cpp/build/
20
+
21
+ # ๆ•ฐๆฎๆ–‡ไปถ
22
+ data/voices/
23
+ *.wav
24
+ *.mp3
25
+
26
+ # ็Žฏๅขƒ
27
+ .env
28
+ .venv/
29
+ venv/
30
+
31
+ # ไธดๆ—ถๆ–‡ไปถ
32
+ *.tmp
33
+ *.log
34
+ debug*.txt
35
+
36
+ # ๆฃ€ๆŸฅ่„šๆœฌ๏ผˆไธ€ๆฌกๆ€งไฝฟ็”จ๏ผ‰
37
+ _check_hf.py
38
+ _download_models.py
docs/llama_cpp_minicpm_ๆŠ€ๆœฏๆŠฅๅ‘Š.md ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llama.cpp ้ƒจ็ฝฒ MiniCPM-o 4.5 ๆŠ€ๆœฏๆŠฅๅ‘Š
2
+
3
+ > ๆœฌๆ–‡ๆกฃ่ฎฐๅฝ•ๅœจ Windows ็Žฏๅขƒไธ‹ไฝฟ็”จ llama.cpp ็ผ–่ฏ‘ใ€้ƒจ็ฝฒ MiniCPM-o 4.5 ๆจกๅž‹็š„ๅ…จ่ฟ‡็จ‹๏ผŒไปฅๅŠๅŽ็ปญ Modal ไบ‘็ซฏ้ƒจ็ฝฒ็š„่ฎพ่ฎกๆ–นๆกˆใ€‚ไฝœไธบๆŽ’ๅ‘ๆ‰‹ๅ†Œๅ’ŒๆŠ€ๆœฏๅ‚่€ƒใ€‚
4
+
5
+ ---
6
+
7
+ ## ไธ€ใ€็ŽฏๅขƒไธŽ็‰ˆๆœฌ
8
+
9
+ | ้กน็›ฎ | ๅ€ผ |
10
+ |------|-----|
11
+ | ๆ“ไฝœ็ณป็ปŸ | Windows 11 |
12
+ | ็ผ–่ฏ‘ๅ™จ | CMake + MSVC (Visual Studio) |
13
+ | llama.cpp ็‰ˆๆœฌ | master ๅˆ†ๆ”ฏ (2026-06-08) |
14
+ | MiniCPM-o 4.5 | HuggingFace `openbmb/MiniCPM-o-4_5` |
15
+ | GGUF ้‡ๅŒ–ๆ–นๅผ | Q4_K_M (ๆ–‡ๆœฌ) + F16 (ๅคšๆจกๆ€ๆŠ•ๅฝฑๅฑ‚) |
16
+ | Python | 3.11 |
17
+
18
+ ---
19
+
20
+ ## ไบŒใ€็ผ–่ฏ‘ llama.cpp โ€”โ€” ่ธฉๅ‘่ฎฐๅฝ•
21
+
22
+ ### 2.1 ็ผ–่ฏ‘็›ฎๆ ‡
23
+
24
+ ้œ€่ฆ็ผ–่ฏ‘ไธคไธช็›ฎๆ ‡๏ผš
25
+ - `llama-server`๏ผšOpenAI ๅ…ผๅฎน็š„ HTTP API ๆœๅŠกๅ™จ
26
+ - `llama-mtmd-cli`๏ผšๅคšๆจกๆ€๏ผˆๅ›พ็‰‡/้Ÿณ้ข‘๏ผ‰ๆŽจ็†ๅ‘ฝไปค่กŒๅทฅๅ…ท
27
+
28
+ ### 2.2 CMake ็ผ–่ฏ‘ๅ‘ฝไปค
29
+
30
+ ```powershell
31
+ cd llama.cpp
32
+ mkdir build
33
+ cd build
34
+ cmake .. -DBUILD_SHARED_LIBS=OFF `
35
+ -DCMAKE_BUILD_TYPE=Release `
36
+ -DGGML_CUDA=OFF `
37
+ -DLLAMA_CURL=ON `
38
+ -DLLAMA_BUILD_SERVER=ON `
39
+ -DLLAMA_BUILD_TESTS=OFF `
40
+ -DLLAMA_BUILD_EXAMPLES=OFF
41
+ cmake --build . --config Release -j 16 --target llama-server llama-mtmd-cli
42
+ ```
43
+
44
+ ### 2.3 ๅ…ณ้”ฎ็ผ–่ฏ‘้€‰้กน่ฏดๆ˜Ž
45
+
46
+ | ้€‰้กน | ่ฏดๆ˜Ž | ไธบไป€ไนˆ |
47
+ |------|------|--------|
48
+ | `DBUILD_SHARED_LIBS=OFF` | ้™ๆ€้“พๆŽฅ | ้ƒจ็ฝฒๆ—ถไธ้œ€่ฆ้ขๅค–็š„ dll/so ไพ่ต– |
49
+ | `DGGML_CUDA=OFF` | ็ฆ็”จ CUDA | Modal ไธŠไฝฟ็”จ A100 ไฝ†ไธๆ˜ฏ็ผ–่ฏ‘ๆ—ถ้“พๆŽฅ CUDA๏ผˆModal ๆœ‰่‡ชๅทฑ้ฉฑๅŠจ๏ผ‰ |
50
+ | `DLLAMA_CURL=ON` | ๅฏ็”จ CURL ๆ”ฏๆŒ | llama-server ้œ€่ฆ CURL ๆฅไธ‹่ฝฝ/ๆตๅผไผ ่พ“ๆจกๅž‹ |
51
+ | `DLLAMA_BUILD_SERVER=ON` | ๆž„ๅปบ HTTP server | ่ฟ™ๆ˜ฏๆˆ‘ไปฌ็š„ไธป่ฆๆœๅŠกๅฝขๅผ |
52
+ | `DCMAKE_BUILD_TYPE=Release` | Release ๆจกๅผ | ็”Ÿไบง็Žฏๅขƒ๏ผŒๅผ€ๅฏไผ˜ๅŒ– |
53
+
54
+ ### 2.4 ่ธฉๅ‘๏ผšCMake ๆ‰พไธๅˆฐ็ผ–่ฏ‘ๅ™จ
55
+
56
+ **้—ฎ้ข˜**๏ผšๅœจ Windows ไธŠ็›ดๆŽฅ่ฟ่กŒ `cmake` ๆŠฅ้”™ๆ‰พไธๅˆฐ C ็ผ–่ฏ‘ๅ™จใ€‚
57
+
58
+ **่งฃๅ†ณ**๏ผš้œ€่ฆไปŽ Visual Studio ็š„ Developer Command Prompt ไธญ่ฟ่กŒ๏ผŒๆˆ–่€…ไฝฟ็”จ `cmake -G "Visual Studio 17 2022"` ๆŒ‡ๅฎš็”Ÿๆˆๅ™จใ€‚
59
+
60
+ ### 2.5 ่ธฉๅ‘๏ผšllama-mtmd-cli ้ป˜่ฎคไธ็ผ–่ฏ‘
61
+
62
+ **้—ฎ้ข˜**๏ผšllama-mtmd-cli ๆ˜ฏ llama.cpp ็š„ `examples/llama-mtmd/` ็›ฎๅฝ•ไธ‹็š„ๅทฅๅ…ท๏ผŒ้ป˜่ฎคไธไผš้š `cmake` ๆž„ๅปบใ€‚
63
+
64
+ **่งฃๅ†ณ**๏ผš็Žฐไปฃ llama.cpp๏ผˆ2025+๏ผ‰ๅทฒ็ปๅฐ† llama-mtmd-cli ็บณๅ…ฅ้กถๅฑ‚ CMakeLists.txt๏ผŒๅช้œ€ `--target llama-mtmd-cli` ๅณๅฏใ€‚ๅฆ‚ๆžœๆ˜ฏ่€็‰ˆๆœฌ้œ€่ฆๅŽป `examples/llama-mtmd/` ไธ‹ๅ•็‹ฌ็ผ–่ฏ‘ใ€‚
65
+
66
+ ### 2.6 ่ธฉๅ‘๏ผšWindows ่ทฏๅพ„ๅˆ†้š”็ฌฆ
67
+
68
+ **้—ฎ้ข˜**๏ผšllama.cpp ็š„ `--mmproj` ๅ‚ๆ•ฐๅœจ Windows ไธŠ้œ€่ฆ็‰นๅˆซๆณจๆ„่ทฏๅพ„ๆ ผๅผใ€‚
69
+
70
+ **่งฃๅ†ณ**๏ผšๅ…จ้ƒจไฝฟ็”จ `/` ่ทฏๅพ„ๅˆ†้š”็ฌฆ๏ผˆCMake ๅ’Œ llama.cpp ๅ†…้ƒจ้ƒฝไผšๅค„็†ๅฅฝ๏ผ‰ใ€‚
71
+
72
+ ---
73
+
74
+ ## ไธ‰ใ€ๆจกๅž‹ไธ‹่ฝฝไธŽ GGUF ่ฝฌๆข
75
+
76
+ ### 3.1 ๆจกๅž‹ๆฅๆบ
77
+
78
+ MiniCPM-o 4.5 ๅฎ˜ๆ–น HuggingFace๏ผš`openbmb/MiniCPM-o-4_5`
79
+
80
+ GGUF ้‡ๅŒ–็‰ˆๆœฌ๏ผšๆฅ่‡ช `bartowski/MiniCPM-o-4_5-GGUF`
81
+
82
+ ### 3.2 ๆ–‡ไปถๆธ…ๅ•๏ผˆ10 ไธชๆ–‡ไปถ๏ผŒ4.3 GB๏ผ‰
83
+
84
+ | ๆ–‡ไปถ | ๅคงๅฐ | ็”จ้€” | ่ฏดๆ˜Ž |
85
+ |------|------|------|------|
86
+ | `MiniCPM-o-4_5-Q4_K_M.gguf` | 688 MB | ไธปๆจกๅž‹๏ผˆLLM ๆ–‡ๆœฌๆŽจ็†๏ผ‰ | Q4_K_M ้‡ๅŒ–๏ผŒๅนณ่กก้€ŸๅบฆไธŽ่ดจ้‡ |
87
+ | `vision/MiniCPM-o-4_5-vision-F16.gguf` | 1044 MB | ่ง†่ง‰็ผ–็ ๅ™จๆŠ•ๅฝฑๅฑ‚ | F16 ไฟๆŒ่ง†่ง‰็ฒพๅบฆ |
88
+ | `audio/MiniCPM-o-4_5-audio-F16.gguf` | 630 MB | ้Ÿณ้ข‘็ผ–็ ๅ™จๆŠ•ๅฝฑๅฑ‚ | F16 ไฟๆŒ้Ÿณ้ข‘็ฒพๅบฆ |
89
+ | `tts/MiniCPM-o-4_5-tts-F16.gguf` | 1104 MB | TTS ่ฏญ้Ÿณๅˆๆˆๆจกๅž‹ | F16 ไฟ่ฏ่ฏญ้Ÿณ่ดจ้‡ |
90
+ | `tts/MiniCPM-o-4_5-projector-F16.gguf` | 14 MB | ๅฃฐๅญฆๆŠ•ๅฝฑๅฑ‚ | ๅฐๆ–‡ไปถ๏ผŒไฝ†ๅฟ…้œ€ |
91
+ | `token2wav-gguf/encoder.gguf` | 144 MB | TTS ็ผ–็ ๅ™จ | token โ†’ mel-spectrogram |
92
+ | `token2wav-gguf/flow_extra.gguf` | 13 MB | TTS flow ้™„ๅŠ ๅ‚ๆ•ฐ | |
93
+ | `token2wav-gguf/flow_matching.gguf` | 437 MB | TTS flow matching | ็”Ÿๆˆ้ซ˜่ดจ้‡่ฏญ้Ÿณ็š„ๆ ธๅฟƒ |
94
+ | `token2wav-gguf/hifigan2.gguf` | 79 MB | HiFi-GAN ๅฃฐ็ ๅ™จ | mel โ†’ wav |
95
+ | `token2wav-gguf/prompt_cache.gguf` | 202 MB | Prompt ็ผ“ๅญ˜ | ๅŠ ้€Ÿ TTS ๆŽจ็† |
96
+
97
+ ### 3.3 ่ธฉๅ‘๏ผšGGUF ไธ‹่ฝฝ้—ฎ้ข˜
98
+
99
+ 1. **HuggingFace ้™้€Ÿ**๏ผš็›ดๆŽฅไธ‹่ฝฝๅคงๆ–‡ไปถๅฏ่ƒฝๅพˆๆ…ข๏ผŒๅปบ่ฎฎไฝฟ็”จ `huggingface_hub` ๅบ“ๆˆ– `hf_transfer` ๅŠ ้€Ÿ
100
+ 2. **ๆ–‡ไปถๅฎŒๆ•ดๆ€ง**๏ผšไธ‹่ฝฝๅŽๅŠกๅฟ…็”จ `sha256sum` ้ชŒ่ฏ๏ผˆGGUF ๆ–‡ไปถๅ†…็ฝฎๆ ก้ชŒ๏ผ‰
101
+ 3. **็›ฎๅฝ•็ป“ๆž„**๏ผšไธ‹่ฝฝๆ—ถไฟๆŒๅŽŸๅง‹็›ฎๅฝ•็ป“ๆž„๏ผˆvision/ใ€audio/ใ€tts/ใ€token2wav-gguf/ ๅญ็›ฎๅฝ•๏ผ‰๏ผŒllama-mtmd-cli ไพ่ต–่ฟ™ไบ›่ทฏๅพ„
102
+
103
+ ---
104
+
105
+ ## ๅ››ใ€ๆœฌๅœฐๆŽจ็†ๆต‹่ฏ•๏ผˆllama-mtmd-cli๏ผ‰
106
+
107
+ ### 4.1 ๅŸบๆœฌๆ–‡ๆœฌๆŽจ็†
108
+
109
+ ```powershell
110
+ .\llama-mtmd-cli.exe `
111
+ -m ..\models\MiniCPM-o-4_5-gguf\MiniCPM-o-4_5-Q4_K_M.gguf `
112
+ --mmproj ..\models\MiniCPM-o-4_5-gguf\vision\MiniCPM-o-4_5-vision-F16.gguf `
113
+ -ngl 99 -c 4096 -n 256 `
114
+ -p "ไปŠๅคฉๅญ•ๅฆ‡้€‚ๅˆๅƒไป€ไนˆ่œ๏ผŸ"
115
+ ```
116
+
117
+ ### 4.2 ๅ›พ็‰‡็†่งฃๆŽจ็†
118
+
119
+ ```powershell
120
+ .\llama-mtmd-cli.exe `
121
+ -m ..\models\MiniCPM-o-4_5-gguf\MiniCPM-o-4_5-Q4_K_M.gguf `
122
+ --mmproj ..\models\MiniCPM-o-4_5-gguf\vision\MiniCPM-o-4_5-vision-F16.gguf `
123
+ -ngl 99 -c 4096 -n 512 `
124
+ --image ..\test_image.jpg `
125
+ -p "ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡ไธญ็š„้ฃŸ็‰ฉ"
126
+ ```
127
+
128
+ ### 4.3 ่ธฉๅ‘๏ผš--mmproj ๅ‚ๆ•ฐ็†่งฃ
129
+
130
+ **้—ฎ้ข˜**๏ผš`--mmproj` ๅช้œ€่ฆไผ ่ง†่ง‰ๆŠ•ๅฝฑๅฑ‚ๆ–‡ไปถ๏ผˆ`MiniCPM-o-4_5-vision-F16.gguf`๏ผ‰๏ผŒไธ้œ€่ฆไผ ๅคšไธชใ€‚
131
+
132
+ **็†่งฃ**๏ผš
133
+ - **่ง†่ง‰ๆŽจ็†**๏ผšไธปๆจกๅž‹ + ่ง†่ง‰ๆŠ•ๅฝฑๅฑ‚
134
+ - **้Ÿณ้ข‘ๆŽจ็†**๏ผšไธปๆจกๅž‹ + ้Ÿณ้ข‘ๆŠ•ๅฝฑๅฑ‚
135
+ - **TTS ็”Ÿๆˆ**๏ผšไธปๆจกๅž‹ + TTS ๆจกๅž‹ + token2wav ็ป„ไปถ
136
+ - ๅ•ๆฌกๆŽจ็†ๅช้œ€่ฆๅŠ ่ฝฝๅฏนๅบ”็š„ๆŠ•ๅฝฑๅฑ‚
137
+
138
+ ### 4.4 ่ธฉๅ‘๏ผš-ngl ๅ‚ๆ•ฐ
139
+
140
+ **้—ฎ้ข˜**๏ผš`-ngl`๏ผˆnumber of GPU layers๏ผ‰่ฎพ็ฝฎไธๅฝ“ไผšๅฏผ่‡ด OOM ๆˆ–ๆŽจ็†ๆžๆ…ขใ€‚
141
+
142
+ **่ง„ๅˆ™**๏ผš
143
+ - `-ngl 99` = ๅฐ†ๆ‰€ๆœ‰ๅฑ‚ๅŠ ่ฝฝๅˆฐ GPU๏ผˆ้œ€่ฆ่ถณๅคŸๆ˜พๅญ˜๏ผŒ688MB Q4 ๆจกๅž‹ โ‰ˆ 2-4GB ๆ˜พๅญ˜๏ผ‰
144
+ - `-ngl 0` = ็บฏ CPU ๆŽจ็†๏ผˆๆžๆ…ข๏ผŒไธๆŽจ่๏ผ‰
145
+ - ๅฆ‚ๆžœๆœ‰็‹ฌ็ซ‹ๆ˜พๅก๏ผŒๅปบ่ฎฎ่‡ณๅฐ‘ `-ngl 33`๏ผˆ็บฆ 1/3 ๅฑ‚ๅˆฐ GPU๏ผ‰
146
+
147
+ ---
148
+
149
+ ## ไบ”ใ€llama-server ้ƒจ็ฝฒ
150
+
151
+ ### 5.1 ๅฏๅŠจๅ‘ฝไปค
152
+
153
+ ```powershell
154
+ .\llama-server.exe `
155
+ -m ..\models\MiniCPM-o-4_5-gguf\MiniCPM-o-4_5-Q4_K_M.gguf `
156
+ --host 0.0.0.0 --port 8080 `
157
+ -ngl 99 -c 8192 `
158
+ --mlock --no-mmap
159
+ ```
160
+
161
+ ### 5.2 ๆไพ›็š„ API ็ซฏ็‚น
162
+
163
+ | ็ซฏ็‚น | ๆ–นๆณ• | ๅŠŸ่ƒฝ | OpenAI ๅ…ผๅฎน |
164
+ |------|------|------|-------------|
165
+ | `/health` | GET | ๅฅๅบทๆฃ€ๆŸฅ | - |
166
+ | `/v1/chat/completions` | POST | ๆ–‡ๆœฌๅฏน่ฏ | โœ… |
167
+ | `/v1/embeddings` | POST | ๆ–‡ๆœฌๅตŒๅ…ฅ | โœ… |
168
+ | `/v1/models` | GET | ๆจกๅž‹ๅˆ—่กจ | โœ… |
169
+ | `/completion` | POST | ๆ–‡ๆœฌ่กฅๅ…จ | โœ… |
170
+ | `/tokenize` | POST | ๅˆ†่ฏ | โœ… |
171
+
172
+ ### 5.3 ่ธฉๅ‘๏ผš--mlock ๅ’Œ --no-mmap
173
+
174
+ **้—ฎ้ข˜**๏ผšๅœจ Modal ็ญ‰ๅฎนๅ™จ็Žฏๅขƒไธญ๏ผŒ้ป˜่ฎค็š„ mmap ๅฏ่ƒฝๅฏผ่‡ดๆ€ง่ƒฝ้—ฎ้ข˜ใ€‚
175
+
176
+ **่งฃๅ†ณ**๏ผš
177
+ - `--mlock`๏ผšๅฐ†ๆจกๅž‹้”ๅฎšๅœจๅ†…ๅญ˜ไธญ๏ผŒ้˜ฒๆญข่ขซ swap
178
+ - `--no-mmap`๏ผšไฝฟ็”จๆ ‡ๅ‡†ๆ–‡ไปถ I/O ่€Œไธๆ˜ฏๅ†…ๅญ˜ๆ˜ ๅฐ„๏ผˆModal ็š„ tmpfs ไธ้€‚ๅˆ mmap๏ผ‰
179
+
180
+ ### 5.4 ่ธฉๅ‘๏ผšๅคšๆจกๆ€ API ้™ๅˆถ
181
+
182
+ **้—ฎ้ข˜**๏ผšllama-server ็š„ `/v1/chat/completions` ็ซฏ็‚น**ไธๆ”ฏๆŒๅ›พ็‰‡/้Ÿณ้ข‘่พ“ๅ…ฅ**๏ผŒๅฎƒๅชๆ˜ฏ็บฏๆ–‡ๆœฌ APIใ€‚ๅคšๆจกๆ€้œ€่ฆ llama-mtmd-cliใ€‚
183
+
184
+ **่งฃๅ†ณๆ–นๆกˆ**๏ผšๅœจ Modal ้ƒจ็ฝฒไธญ๏ผŒๆˆ‘ไปฌๅŒ…่ฃ…ไบ†ไธคๅฑ‚๏ผš
185
+ 1. ็บฏๆ–‡ๆœฌ่ฏทๆฑ‚ โ†’ ็›ดๆŽฅไปฃ็†ๅˆฐ llama-server ็š„ `/v1/chat/completions`
186
+ 2. ๅคšๆจกๆ€่ฏทๆฑ‚ โ†’ ่ฐƒ็”จ llama-mtmd-cli ๅญ่ฟ›็จ‹ๅค„็†
187
+
188
+ ---
189
+
190
+ ## ๅ…ญใ€Modal ไบ‘็ซฏ้ƒจ็ฝฒ่ฎพ่ฎก
191
+
192
+ ### 6.1 ๆžถๆž„
193
+
194
+ ```
195
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
196
+ โ”‚ Modal Cloud โ”‚
197
+ โ”‚ โ”‚
198
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
199
+ โ”‚ โ”‚ FastAPI ASGI App โ”‚ โ”‚ Model Volume โ”‚ โ”‚
200
+ โ”‚ โ”‚ (serve function) โ”‚ โ”‚ (ๆŒไน…ๅŒ–ๅญ˜ๅ‚จ) โ”‚ โ”‚
201
+ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚
202
+ โ”‚ โ”‚ /v1/chat/completionsโ”‚โ”€โ”€โ”œโ”€โ”€โ”‚ GGUF ๆจกๅž‹ๆ–‡ไปถ โ”‚ โ”‚ โ”‚
203
+ โ”‚ โ”‚ /v1/multimodal/chat โ”‚ โ”‚ โ”‚ (10 files) โ”‚ โ”‚ โ”‚
204
+ โ”‚ โ”‚ /v1/embeddings โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚
205
+ โ”‚ โ”‚ /health โ”‚ โ”‚ โ”‚ โ”‚
206
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
207
+ โ”‚ โ”‚ โ”‚
208
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
209
+ โ”‚ โ”‚ llama-server โ”‚ โ† ็บฏๆ–‡ๆœฌๆŽจ็† โ”‚
210
+ โ”‚ โ”‚ llama-mtmd-cli โ”‚ โ† ๅคšๆจกๆ€ๆŽจ็† (ๆŒ‰้œ€ๆ‹‰่ตท) โ”‚
211
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
212
+ โ”‚ โ”‚
213
+ โ”‚ GPU: A100 (40GB) โ”‚
214
+ โ”‚ ๅฎนๅ™จ็”Ÿๅ‘ฝๅ‘จๆœŸ: ็ฉบ้—ฒ 5min โ†’ ่‡ชๅŠจๅ…ณ้—ญ โ”‚
215
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
216
+ ```
217
+
218
+ ### 6.2 Modal ้…็ฝฎ่ฆ็‚น
219
+
220
+ | ๅ‚ๆ•ฐ | ๅ€ผ | ๅŽŸๅ›  |
221
+ |------|-----|------|
222
+ | `gpu` | `A100` | MiniCPM-o 4.5 ้œ€่ฆ GPU ๆŽจ็† |
223
+ | `container_idle_timeout` | `300` ็ง’ | ๆŽงๅˆถๆˆๆœฌ๏ผŒๆ— ไบบไฝฟ็”จๆ—ถ่‡ชๅŠจๅ…ณๆœบ |
224
+ | `allow_concurrent_inputs` | `10` | ๆ”ฏๆŒๅคš็”จๆˆทๅŒๆ—ถ่ฏทๆฑ‚ |
225
+ | `timeout` | `600` ็ง’ | ๅ•ๆฌก่ฏทๆฑ‚่ถ…ๆ—ถ 10 ๅˆ†้’Ÿ๏ผˆๅซๆจกๅž‹ๅŠ ่ฝฝ๏ผ‰ |
226
+ | ้•œๅƒ | Debian Slim + Python 3.11 + llama.cpp ็ผ–่ฏ‘ | ่‡ชๅฎšไน‰ Docker ้•œๅƒ |
227
+
228
+ ### 6.3 Model Volume ่ฎพ่ฎก
229
+
230
+ **ไธบไป€ไนˆ็”จ Volume ่€Œไธๆ˜ฏ้•œๅƒไธญๆ‰“ๅŒ…ๆจกๅž‹**๏ผš
231
+ 1. ๆจกๅž‹ๆ–‡ไปถ 4.3 GB๏ผŒๆ‰“ๅŒ…่ฟ›้•œๅƒไผšไฝฟ้•œๅƒ่ฟ‡ๅคง
232
+ 2. ๆจกๅž‹ๆ›ดๆ–ฐ็š„้ข‘็އ้ซ˜ไบŽไปฃ็ ๆ›ดๆ–ฐ๏ผŒๅˆ†ๅผ€็ฎก็†ๆ›ด็ตๆดป
233
+ 3. Volume ๅฏไปฅๅœจๅคšไธช App ้—ดๅ…ฑไบซ
234
+
235
+ **ไธŠไผ ๆจกๅž‹ๅˆฐ Volume**๏ผš
236
+ ```bash
237
+ modal volume put minicpm-o-4_5-models ./models/MiniCPM-o-4_5-gguf /
238
+ ```
239
+
240
+ ### 6.4 ่ธฉๅ‘๏ผšModal ไธŠ็ผ–่ฏ‘ llama.cpp
241
+
242
+ **้—ฎ้ข˜**๏ผšModal ๅฎนๅ™จๆ˜ฏ Linux๏ผŒ่€Œๆˆ‘ไปฌๆœฌๅœฐๆ˜ฏ Windows ็ผ–่ฏ‘็š„ .exe๏ผŒไธ่ƒฝ็›ดๆŽฅไฝฟ็”จใ€‚
243
+
244
+ **่งฃๅ†ณ**๏ผšๅœจ Modal ็š„ `Image.run_commands()` ไธญ็›ดๆŽฅ็ผ–่ฏ‘ llama.cpp๏ผš
245
+
246
+ ```python
247
+ llamacpp_image = (
248
+ Image.debian_slim(python_version="3.11")
249
+ .apt_install("curl", "git", "build-essential", "cmake", "libcurl4-openssl-dev")
250
+ .run_commands(
251
+ "git clone --depth 1 https://github.com/ggerganov/llama.cpp /llama.cpp",
252
+ "cd /llama.cpp && cmake -B build ...",
253
+ "cd /llama.cpp && cmake --build build --config Release -j $(nproc) ...",
254
+ )
255
+ )
256
+ ```
257
+
258
+ **ๅ…ณ้”ฎ**๏ผšๅฟ…้กปๆœ‰ `libcurl4-openssl-dev` ๅŒ…๏ผŒๅฆๅˆ™ `LLAMA_CURL=ON` ไผš็ผ–่ฏ‘ๅคฑ่ดฅใ€‚
259
+
260
+ ### 6.5 ่ธฉๅ‘๏ผšModal ็š„ๅ†ทๅฏๅŠจ
261
+
262
+ **้—ฎ้ข˜**๏ผšModal ๅœจ็ฉบ้—ฒๅŽๅ…ณ้—ญๅฎนๅ™จ๏ผŒไธ‹ๆฌก่ฏทๆฑ‚้œ€่ฆ้‡ๆ–ฐๅฏๅŠจ๏ผŒๅฏผ่‡ด้ฆ–ๆฌกๅ“ๅบ”ๅปถ่ฟŸ 30-60 ็ง’๏ผˆ้•œๅƒๅŠ ่ฝฝ + ๆจกๅž‹ไปŽ Volume ๅŠ ่ฝฝๅˆฐ GPU๏ผ‰ใ€‚
263
+
264
+ **็ผ“่งฃๆ–นๆกˆ**๏ผš
265
+ 1. ่ฎพ็ฝฎ `container_idle_timeout` ไธ่ฆๅคช็Ÿญ๏ผˆๅปบ่ฎฎ 300-600 ็ง’๏ผ‰
266
+ 2. ๅœจๅฎขๆˆท็ซฏๆทปๅŠ ๅฅๅบทๆฃ€ๆŸฅ + ้‡่ฏ•ๆœบๅˆถ
267
+ 3. ๅฏนไบŽไธๅคช้ข‘็น็š„่ฐƒ็”จๅœบๆ™ฏ๏ผŒๆŽฅๅ—ๅ†ทๅฏๅŠจๅปถ่ฟŸ
268
+
269
+ ### 6.6 ๅฐšๆœช่งฃๅ†ณ็š„้—ฎ้ข˜
270
+
271
+ 1. **้Ÿณ้ข‘่พ“ๅ…ฅๅค„็†**๏ผšllama-mtmd-cli ็š„้Ÿณ้ข‘่พ“ๅ…ฅๆŽฅๅฃ้œ€่ฆ่ฟ›ไธ€ๆญฅๆต‹่ฏ•
272
+ 2. **TTS ่ฏญ้Ÿณ่พ“ๅ‡บ**๏ผštoken2wav ็ป„ไปถๅœจ llama.cpp ไธญ็š„้›†ๆˆๆ–นๅผ้œ€่ฆ้ชŒ่ฏ
273
+ 3. **Modal ๅฎ‰่ฃ…ๅคฑ่ดฅ**๏ผšๅฝ“ๅ‰็ฝ‘็ปœ SSL ้—ฎ้ข˜ๅฏผ่‡ด `pip install modal` ๅคฑ่ดฅ๏ผŒ้œ€ๅœจ็ฝ‘็ปœๆขๅคๅŽๅฎ‰่ฃ…ๅนถ้ƒจ็ฝฒ
274
+
275
+ ---
276
+
277
+ ## ไธƒใ€ๅฎขๆˆท็ซฏๅฐ่ฃ…่ฎพ่ฎก
278
+
279
+ ### 7.1 Python ๅฎขๆˆท็ซฏ๏ผˆ`modal_deploy/client.py`๏ผ‰
280
+
281
+ ๅฐ่ฃ…ไบ†ไปฅไธ‹่ƒฝๅŠ›๏ผš
282
+ - `chat()` / `ask()`๏ผšๆ–‡ๆœฌๅฏน่ฏ
283
+ - `chat_with_image()` / `describe_image()`๏ผšๅ›พ็‰‡็†่งฃ
284
+ - `embed()`๏ผšๆ–‡ๆœฌๅตŒๅ…ฅ
285
+ - ้ข„่ฎพ System Prompt๏ผˆๅญ•ๆœŸ่ฅๅ…ป้กพ้—ฎใ€ๅฎถๅบญ่ฅๅ…ปๅธˆใ€้ฃŸ็‰ฉๅˆ†ๆžๅ™จใ€่ฅๅ…ปๆ€ป็ป“๏ผ‰
286
+
287
+ ### 7.2 ไธŽ PregoPal ้›†ๆˆ
288
+
289
+ ```python
290
+ from modal_deploy.client import MiniCPMClient, SYSTEM_PROMPTS
291
+
292
+ client = MiniCPMClient(base_url="https://your-app.modal.run")
293
+
294
+ # ๅญ•ๆœŸ้ฅฎ้ฃŸ่ฏข้—ฎ
295
+ reply = client.ask(
296
+ "ๆˆ‘ไปŠๅคฉๅฏไปฅๅƒไป€ไนˆ๏ผŸ",
297
+ system_prompt=SYSTEM_PROMPTS["diet_advisor"]
298
+ )
299
+
300
+ # ๅ›พ็‰‡ไธญ็š„้ฃŸ็‰ฉๅˆ†ๆž
301
+ import base64
302
+ with open("food.jpg", "rb") as f:
303
+ img_b64 = base64.b64encode(f.read()).decode()
304
+ result = client.chat_with_image("่ฟ™ๆ˜ฏไป€ไนˆ้ฃŸ็‰ฉ๏ผŸ้€‚ๅˆๅญ•ๅฆ‡ๅ—๏ผŸ", img_b64)
305
+ ```
306
+
307
+ ---
308
+
309
+ ## ๅ…ซใ€ๆˆๆœฌไผฐ็ฎ—
310
+
311
+ | ้กน็›ฎ | ไผฐ็ฎ— |
312
+ |------|------|
313
+ | A100 GPU ๅ•ไปท | ~$1.10/ๅฐๆ—ถ |
314
+ | ๆจกๅž‹ๅŠ ่ฝฝๆ—ถ้—ด | ~20-30 ็ง’ |
315
+ | ๅ•ๆฌกๆŽจ็†ๅปถ่ฟŸ | ~2-5 ็ง’๏ผˆๆ–‡ๆœฌ๏ผ‰/ ~5-10 ็ง’๏ผˆๅคšๆจกๆ€๏ผ‰ |
316
+ | ๆœˆ้ข„ไผฐ๏ผˆๆฏๆ—ฅ 100 ๆฌกๆŽจ็†๏ผ‰ | ~$20-50๏ผˆ่€ƒ่™‘ๅ†ทๅฏๅŠจๅ’Œ็ฉบ้—ฒ๏ผ‰ |
317
+
318
+ ---
319
+
320
+ ## ไนใ€ๅ…ณ้”ฎ่ธฉๅ‘ๆ€ป็ป“
321
+
322
+ | # | ๅ‘ | ๅŽŸๅ›  | ่งฃๅ†ณๆ–นๆกˆ |
323
+ |---|-----|------|---------|
324
+ | 1 | CMake ๆ‰พไธๅˆฐ็ผ–่ฏ‘ๅ™จ | Windows ๆฒกๆœ‰ๅ…จๅฑ€ MSVC ่ทฏๅพ„ | ไฝฟ็”จ VS Developer Command Prompt |
325
+ | 2 | llama-mtmd-cli ไธ็ผ–่ฏ‘ | ้ป˜่ฎค target ไธๅŒ…ๅซ | ๆ˜พๅผๆŒ‡ๅฎš `--target llama-mtmd-cli` |
326
+ | 3 | libcurl ็ผบๅคฑ | `LLAMA_CURL=ON` ้œ€่ฆๅผ€ๅ‘ๅŒ… | `apt install libcurl4-openssl-dev` |
327
+ | 4 | server ไธๆ”ฏๆŒๅคšๆจกๆ€ API | llama-server ๅชๆ˜ฏๆ–‡ๆœฌ | ๆ–‡ๆœฌ็”จ server๏ผŒๅคšๆจกๆ€็”จ mtmd-cli |
328
+ | 5 | GGUF ๆ–‡ไปถ็›ฎๅฝ•็ป“ๆž„ | mtmd-cli ไพ่ต–็‰นๅฎš่ทฏๅพ„ | ไฟๆŒๅŽŸๅง‹็›ฎๅฝ•็ป“ๆž„ |
329
+ | 6 | Modal ้•œๅƒ vs ๆจกๅž‹ๅˆ†็ฆป | ้•œๅƒไธ่ƒฝๅคชๅคง | ็”จ Volume ๅญ˜ๅ‚จๆจกๅž‹ |
330
+ | 7 | ๅ†ทๅฏๅŠจๅปถ่ฟŸ | Modal ้—ฒ็ฝฎ่‡ชๅŠจๅ…ณ้—ญๅฎนๅ™จ | ๅˆ็†่ฎพ็ฝฎ idle_timeout๏ผŒๅฎขๆˆท็ซฏๅŠ ้‡่ฏ• |
331
+ | 8 | pip install modal ๅคฑ่ดฅ | SSL ่ฏไนฆ้—ฎ้ข˜ | ๆข็ฝ‘็ปœ็Žฏๅขƒๆˆ–้…็ฝฎไปฃ็† |
332
+ | 9 | --mmproj ๅ‚ๆ•ฐ็†่งฃ | ๅช้œ€ไผ ไธ€ไธชๆŠ•ๅฝฑๅฑ‚ | ่ง†่ง‰ไผ  vision.gguf๏ผŒ้Ÿณ้ข‘ไผ  audio.gguf |
333
+ | 10 | Windows ่ทฏๅพ„ไธญ็š„็ฉบๆ ผ | MSVC ่ทฏๅพ„ๅซ็ฉบๆ ผ | ไฝฟ็”จๅŒๅผ•ๅทๅŒ…่ฃน่ทฏๅพ„ |
334
+
335
+ ---
336
+
337
+ *ๆ–‡ๆกฃ็”Ÿๆˆๆ—ถ้—ด๏ผš2026-06-09 | v1.0 - llama.cpp + MiniCPM-o ้ƒจ็ฝฒๅ…จ่ฎฐๅฝ•*
docs/ๅผ€ๅ‘ๆ—ฅๅฟ—.md CHANGED
@@ -61,3 +61,65 @@ PregoPal/
61
  - ๆ‰€ๆœ‰ 5 ไธช Tab ๆญฃๅธธๅŠ ่ฝฝ
62
  - ๅฃฐ็บน่ฏ†ๅˆซใ€่œๅ“ๆŽจ่ใ€้ฅฎ้ฃŸ่ฎฐๅฝ•ใ€่ฅๅ…ปๆŠฅๅ‘ŠๅŠŸ่ƒฝๅฏ็”จ
63
  - core/ ๅฑ‚้ข„็•™ๆŽฅๅฃ็ญ‰ๅพ… MiniCPM-o ้ƒจ็ฝฒ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  - ๆ‰€ๆœ‰ 5 ไธช Tab ๆญฃๅธธๅŠ ่ฝฝ
62
  - ๅฃฐ็บน่ฏ†ๅˆซใ€่œๅ“ๆŽจ่ใ€้ฅฎ้ฃŸ่ฎฐๅฝ•ใ€่ฅๅ…ปๆŠฅๅ‘ŠๅŠŸ่ƒฝๅฏ็”จ
63
  - core/ ๅฑ‚้ข„็•™ๆŽฅๅฃ็ญ‰ๅพ… MiniCPM-o ้ƒจ็ฝฒ
64
+
65
+ ## 2026-06-09 ๅ‡Œๆ™จ๏ผšllama.cpp ็ผ–่ฏ‘ + MiniCPM-o GGUF ๆจกๅž‹ไธ‹่ฝฝ
66
+
67
+ ### ็ผ–่ฏ‘็Žฏๅขƒ
68
+ - ๆ‹‰ๅ– llama.cpp master ๅˆ†ๆ”ฏ๏ผˆ2026-06-08๏ผ‰
69
+ - CMake + MSVC (Visual Studio 2022) ็ผ–่ฏ‘
70
+ - ็›ฎๆ ‡๏ผš`llama-server` + `llama-mtmd-cli`
71
+ - ็ผ–่ฏ‘้€‰้กน๏ผš`DLLAMA_CURL=ON`, `DLLAMA_BUILD_SERVER=ON`, `DGGML_CUDA=OFF`
72
+
73
+ ### ๆจกๅž‹ไธ‹่ฝฝ
74
+ - ๆฅๆบ๏ผšHuggingFace `bartowski/MiniCPM-o-4_5-GGUF`
75
+ - ไธปๆจกๅž‹๏ผš`MiniCPM-o-4_5-Q4_K_M.gguf` (688 MB)
76
+ - ่ง†่ง‰ๆŠ•ๅฝฑๅฑ‚๏ผš`vision/MiniCPM-o-4_5-vision-F16.gguf` (1044 MB)
77
+ - ้Ÿณ้ข‘ๆŠ•ๅฝฑๅฑ‚๏ผš`audio/MiniCPM-o-4_5-audio-F16.gguf` (630 MB)
78
+ - TTS ๆจกๅž‹๏ผš`tts/MiniCPM-o-4_5-tts-F16.gguf` (1104 MB) + projector (14 MB)
79
+ - token2wav ็ป„ไปถ๏ผš5 ไธชๆ–‡ไปถ๏ผˆencoder, flow_extra, flow_matching, hifigan2, prompt_cache๏ผ‰
80
+ - ๅˆ่ฎก 10 ไธชๆ–‡ไปถ๏ผŒ4.3 GB
81
+
82
+ ### ่ธฉๅ‘่ฎฐๅฝ•
83
+ 1. CMake ๅœจ Windows ๆ™ฎ้€š cmd ไธญๆ‰พไธๅˆฐ MSVC ็ผ–่ฏ‘ๅ™จ โ†’ ้œ€่ฆ VS Developer Command Prompt
84
+ 2. llama-mtmd-cli ้œ€ๆ˜พๅผๆŒ‡ๅฎš `--target` ๅฆๅˆ™ไธ็ผ–่ฏ‘
85
+ 3. `libcurl4-openssl-dev` ็ผบๅคฑๅฏผ่‡ด LLAMA_CURL=ON ็ผ–่ฏ‘ๅคฑ่ดฅ๏ผˆLinux ็Žฏๅขƒไธ‹๏ผ‰
86
+ 4. llama-server ็š„ `/v1/chat/completions` ไธๆ”ฏๆŒๅคšๆจกๆ€ โ†’ ๅคšๆจกๆ€้œ€็”จ llama-mtmd-cli
87
+ 5. GGUF ๆ–‡ไปถ้œ€ไฟๆŒๅŽŸๅง‹็›ฎๅฝ•็ป“ๆž„๏ผˆvision/ใ€audio/ใ€tts/ใ€token2wav-gguf/๏ผ‰
88
+ 6. pip install modal ๅ›  SSL ่ฏไนฆ้—ฎ้ข˜ๅคฑ่ดฅ๏ผˆๆธ…ๅŽ้•œๅƒๅ’Œ PyPI ๅฎ˜ๆ–น้ƒฝ่ฟ”ๅ›ž SSLZeroReturnError๏ผ‰
89
+
90
+ ## 2026-06-09 ๅ‡Œๆ™จ๏ผšModal ไบ‘็ซฏ้ƒจ็ฝฒ่ฎพ่ฎก
91
+
92
+ ### ้ƒจ็ฝฒๆžถๆž„
93
+ - FastAPI ASGI App ไฝœไธบๅ…ฅๅฃ๏ผŒๅŒ…่ฃ… llama-server + llama-mtmd-cli
94
+ - ๆจกๅž‹ๅญ˜ๅ‚จๅœจ Modal Volume๏ผˆๆŒไน…ๅŒ–๏ผ‰๏ผŒไปฃ็ ้€š่ฟ‡ App ้ƒจ็ฝฒ
95
+ - GPU: A100
96
+ - ็ฉบ้—ฒ 5 ๅˆ†้’ŸๅŽ่‡ชๅŠจๅ…ณ้—ญไปฅๆŽงๅˆถๆˆๆœฌ
97
+
98
+ ### ๅทฒๅˆ›ๅปบๆ–‡ไปถ
99
+ - `modal_deploy/deploy.py` โ€” Modal ้ƒจ็ฝฒไธป่„šๆœฌ๏ผŒๅซ OpenAI ๅ…ผๅฎน API + ๅคšๆจกๆ€็ซฏ็‚น
100
+ - `modal_deploy/client.py` โ€” Python ๅฎขๆˆท็ซฏๅฐ่ฃ…๏ผŒๅซๅญ•ๆœŸๅœบๆ™ฏ้ข„่ฎพ System Prompt
101
+ - `modal_deploy/README.md` โ€” ้ƒจ็ฝฒ่ฏดๆ˜Žๆ–‡ๆกฃ
102
+ - `docs/llama_cpp_minicpm_ๆŠ€ๆœฏๆŠฅๅ‘Š.md` โ€” ๅฎŒๆ•ดๆŠ€ๆœฏๆŠฅๅ‘Š๏ผˆ็ผ–่ฏ‘/้ƒจ็ฝฒ/ๆŽ’ๅ‘๏ผ‰
103
+ - `requirements.txt` โ€” ่ฟฝๅŠ  modal>=0.60.0
104
+
105
+ ### API ๆŽฅๅฃ่ฎพ่ฎก
106
+ | ็ซฏ็‚น | ๅŠŸ่ƒฝ |
107
+ |------|------|
108
+ | `/v1/chat/completions` | ็บฏๆ–‡ๆœฌๅฏน่ฏ๏ผˆOpenAI ๅ…ผๅฎน๏ผ‰ |
109
+ | `/v1/multimodal/chat` | ๅคšๆจกๆ€ๅฏน่ฏ๏ผˆๅ›พ็‰‡็†่งฃ๏ผ‰ |
110
+ | `/v1/embeddings` | ๆ–‡ๆœฌๅตŒๅ…ฅ |
111
+ | `/health` | ๅฅๅบทๆฃ€ๆŸฅ |
112
+ | `/v1/models` | ๆจกๅž‹ๅˆ—่กจ |
113
+
114
+ ### ๅพ…ๅฎŒๆˆ
115
+ - ็ฝ‘็ปœๆขๅคๅŽ `pip install modal` ๅนถ้ƒจ็ฝฒ
116
+ - ้Ÿณ้ข‘่พ“ๅ…ฅ/่พ“ๅ‡บๆŽฅๅฃๆต‹่ฏ•
117
+ - Token2wav TTS ็ป„ไปถๅœจ llama.cpp ไธญ็š„้›†ๆˆ้ชŒ่ฏ
118
+
119
+ ## 2026-06-09 ๆ™จ้—ด๏ผšๅŒๆญฅๅนถ่กŒ Cline ไปฃ็ 
120
+
121
+ ### ่ฟœ็จ‹ๆ›ดๆ–ฐ๏ผˆ0155d1a โ†’ ed20b95๏ผ‰
122
+ - `README.md` ๅคงๅน…ๆ‰ฉๅ……๏ผˆ+565 ่กŒ๏ผ‰
123
+ - `app.py` ๅพฎ่ฐƒ๏ผˆ+7/-1๏ผ‰
124
+ - `ui/app_builder.py` ้‡ๆž„๏ผˆ+633/-156๏ผ‰
125
+ - `utils.py` ๆ–ฐๅขžๅทฅๅ…ทๅ‡ฝๆ•ฐ๏ผˆ+176 ่กŒ๏ผ‰
modal_deploy/README.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modal Deployment: MiniCPM-o-4_5 via llama.cpp
2
+
3
+ ## ๆฆ‚่ฟฐ
4
+
5
+ ๅœจ Modal.com ็š„ A100 GPU ไธŠ้ƒจ็ฝฒ MiniCPM-o-4_5 ๆจกๅž‹๏ผŒ้€š่ฟ‡ llama.cpp server ๆไพ› OpenAI ๅ…ผๅฎน็š„ API ๆŽฅๅฃใ€‚
6
+
7
+ ## ๆžถๆž„
8
+
9
+ ```
10
+ ็”จๆˆท่ฏทๆฑ‚ โ†’ FastAPI (ASGI) โ†’ llama-server (OpenAI ๅ…ผๅฎน) โ†’ MiniCPM-o-4_5 GGUF
11
+ โ†“
12
+ ็บฏๆ–‡ๆœฌ: /v1/chat/completions
13
+ ๅคšๆจกๆ€: /v1/multimodal/chat (้€š่ฟ‡ llama-mtmd-cli)
14
+ ```
15
+
16
+ ## ๅ‰็ฝฎๆกไปถ
17
+
18
+ 1. Modal ่ดฆๅท: https://modal.com/
19
+ 2. ๅฎ‰่ฃ… Modal CLI: `pip install modal`
20
+ 3. ็™ปๅฝ•: `modal token new`
21
+
22
+ ## ๆจกๅž‹ๆ–‡ไปถ
23
+
24
+ ้œ€่ฆ 10 ไธช GGUF ๆ–‡ไปถ๏ผˆ็บฆ 4.3 GB๏ผ‰๏ผŒไปŽ HuggingFace ไธ‹่ฝฝ๏ผš
25
+
26
+ ```bash
27
+ # ไธ‹่ฝฝๅˆฐๆœฌๅœฐ
28
+ python _download_models.py # ๅœจ PregoPal ๆ น็›ฎๅฝ•
29
+
30
+ # ไธŠไผ ๅˆฐ Modal Volume
31
+ modal volume put minicpm-o-4_5-models ../models/MiniCPM-o-4_5-gguf /
32
+ ```
33
+
34
+ ๆจกๅž‹ๆ–‡ไปถๅˆ—่กจ๏ผš
35
+ | ๆ–‡ไปถ | ๅคงๅฐ | ็”จ้€” |
36
+ |------|------|------|
37
+ | `MiniCPM-o-4_5-Q4_K_M.gguf` | 688 MB | ไธปๆจกๅž‹๏ผˆๆ–‡ๆœฌๆŽจ็†๏ผ‰ |
38
+ | `vision/MiniCPM-o-4_5-vision-F16.gguf` | 1044 MB | ่ง†่ง‰ๆŠ•ๅฝฑๅฑ‚ |
39
+ | `audio/MiniCPM-o-4_5-audio-F16.gguf` | 630 MB | ้Ÿณ้ข‘ๆŠ•ๅฝฑๅฑ‚ |
40
+ | `tts/MiniCPM-o-4_5-tts-F16.gguf` | 1104 MB | TTS ่ฏญ้Ÿณๅˆๆˆ |
41
+ | `tts/MiniCPM-o-4_5-projector-F16.gguf` | 14 MB | ๆŠ•ๅฝฑๅฑ‚ |
42
+ | `token2wav-gguf/*.gguf` (5 ๆ–‡ไปถ) | 875 MB | ่ฏญ้Ÿณๅˆๆˆ tokenโ†’wav |
43
+
44
+ ## ้ƒจ็ฝฒ
45
+
46
+ ```bash
47
+ cd modal_deploy
48
+
49
+ # ้ƒจ็ฝฒไธบ Modal App๏ผˆๆŒ็ปญ่ฟ่กŒ็š„ๆœๅŠก๏ผ‰
50
+ modal deploy deploy.py
51
+
52
+ # ๆˆ–ๅผ€ๅ‘ๆจกๅผ๏ผˆ็ƒญ้‡่ฝฝ๏ผŒๆ—ฅๅฟ—ๅœจๅ‰ๅฐ๏ผ‰
53
+ modal serve deploy.py
54
+ ```
55
+
56
+ ้ƒจ็ฝฒๅŽ Modal ไผš่พ“ๅ‡บไธ€ไธช URL๏ผŒๅฆ‚ `https://your-org--prego-pal-minicpm-serve.modal.run`
57
+
58
+ ## API ๆŽฅๅฃ
59
+
60
+ ### 1. ๅฅๅบทๆฃ€ๆŸฅ
61
+ ```bash
62
+ GET /health
63
+ ```
64
+ ่ฟ”ๅ›ž๏ผš`{"status": "ok"}`
65
+
66
+ ### 2. ๆจกๅž‹ๅˆ—่กจ
67
+ ```bash
68
+ GET /v1/models
69
+ ```
70
+
71
+ ### 3. ๆ–‡ๆœฌๅฏน่ฏ (OpenAI ๅ…ผๅฎน)
72
+ ```bash
73
+ POST /v1/chat/completions
74
+ Content-Type: application/json
75
+
76
+ {
77
+ "model": "MiniCPM-o-4_5",
78
+ "messages": [
79
+ {"role": "user", "content": "ไปŠๅคฉๅญ•ๅฆ‡ๅฏไปฅๅƒไป€ไนˆ๏ผŸ"}
80
+ ],
81
+ "max_tokens": 1024,
82
+ "temperature": 0.7,
83
+ "stream": false
84
+ }
85
+ ```
86
+
87
+ ### 4. ๅคšๆจกๆ€ๅฏน่ฏ๏ผˆๅ›พ็‰‡็†่งฃ๏ผ‰
88
+ ```bash
89
+ POST /v1/multimodal/chat
90
+ Content-Type: application/json
91
+
92
+ {
93
+ "messages": [
94
+ {"role": "user", "content": [
95
+ {"type": "text", "text": "่ฟ™ๅผ ๅ›พ็‰‡้‡Œๆœ‰ไป€ไนˆ้ฃŸ็‰ฉ๏ผŸ"},
96
+ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
97
+ ]}
98
+ ],
99
+ "max_tokens": 1024,
100
+ "temperature": 0.7
101
+ }
102
+ ```
103
+
104
+ ### 5. ๅตŒๅ…ฅๅ‘้‡
105
+ ```bash
106
+ POST /v1/embeddings
107
+ Content-Type: application/json
108
+
109
+ {
110
+ "model": "MiniCPM-o-4_5",
111
+ "input": ["ๅญ•ๆœŸ่ฅๅ…ปๅปบ่ฎฎ"]
112
+ }
113
+ ```
114
+
115
+ ## ๆต‹่ฏ•
116
+
117
+ ```bash
118
+ # ๅฟซ้€Ÿๆต‹่ฏ•ๆจกๅž‹่ƒฝๅฆๅŠ ่ฝฝๅ’ŒๆŽจ็†
119
+ modal run modal_deploy/deploy.py::test_inference
120
+
121
+ # ไธŠไผ ๆจกๅž‹
122
+ modal run modal_deploy/deploy.py::upload_models
123
+ ```
124
+
125
+ ## ้…็ฝฎ
126
+
127
+ ๅœจ `deploy.py` ไธญๅฏ่ฐƒๆ•ด็š„ๅ‚ๆ•ฐ๏ผš
128
+ - `container_idle_timeout=300` โ€” ็ฉบ้—ฒ 5 ๅˆ†้’ŸๅŽ่‡ชๅŠจๅ…ณ้—ญ
129
+ - `gpu="A100"` โ€” GPU ็ฑปๅž‹๏ผˆไนŸๅฏ็”จ H100ใ€A10G๏ผ‰
130
+ - `-c 8192` โ€” ไธŠไธ‹ๆ–‡็ช—ๅฃๅคงๅฐ
131
+ - `-ngl 99` โ€” GPU ๅฑ‚ๆ•ฐ๏ผˆ99=ๅ…จ้ƒจๅˆฐ GPU๏ผ‰
132
+
133
+ ## ไธŽ PregoPal ้›†ๆˆ
134
+
135
+ ๅ…ถไป–ๆœๅŠก้€š่ฟ‡ HTTP ่ฐƒ็”จๆญค API๏ผš
136
+
137
+ ```python
138
+ import requests
139
+
140
+ API_BASE = "https://your-app.modal.run"
141
+
142
+ def ask_minicpm(prompt: str) -> str:
143
+ r = requests.post(f"{API_BASE}/v1/chat/completions", json={
144
+ "messages": [{"role": "user", "content": prompt}],
145
+ "max_tokens": 512,
146
+ "temperature": 0.7,
147
+ })
148
+ return r.json()["choices"][0]["message"]["content"]
149
+ ```
150
+
151
+ ## ๆณจๆ„ไบ‹้กน
152
+
153
+ 1. **ๅ†ทๅฏๅŠจ**๏ผšๆ— ่ฏทๆฑ‚ >5 ๅˆ†้’ŸๅŽๅฎนๅ™จๅ…ณ้—ญ๏ผŒไธ‹ๆฌก่ฏทๆฑ‚้œ€็ญ‰ๅพ… ~30-60 ็ง’๏ผˆๆจกๅž‹ๅŠ ่ฝฝ๏ผ‰
154
+ 2. **ๆˆๆœฌ**๏ผšA100 ็บฆ $1.10/ๅฐๆ—ถ๏ผŒๆŒ‰ๅฎž้™…ไฝฟ็”จ่ฎก่ดน
155
+ 3. **ๆจกๅž‹ไธŠไผ **๏ผšVolume ๆ˜ฏๆŒไน…ๅŒ–็š„๏ผŒๆจกๅž‹ๅช้œ€ไธŠไผ ไธ€ๆฌก
156
+ 4. **ๅนถๅ‘**๏ผš`allow_concurrent_inputs=10` ๆ”ฏๆŒ 10 ไธชๅนถๅ‘่ฏทๆฑ‚
modal_deploy/client.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PregoPal - MiniCPM-o API ๅฎขๆˆท็ซฏ
3
+
4
+ ็”จๆณ•๏ผš
5
+ from modal_deploy.client import MiniCPMClient
6
+
7
+ client = MiniCPMClient(base_url="https://your-app.modal.run")
8
+ response = client.chat("ไปŠๅคฉๅญ•ๅฆ‡ๅฏไปฅๅƒไป€ไนˆ๏ผŸ")
9
+ """
10
+ import json
11
+ import logging
12
+ from typing import Optional
13
+ from dataclasses import dataclass, field
14
+
15
+ import requests
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class ChatMessage:
22
+ role: str
23
+ content: str
24
+
25
+
26
+ @dataclass
27
+ class MultimodalContent:
28
+ type: str # "text" or "image_url"
29
+ text: Optional[str] = None
30
+ image_url: Optional[dict] = None
31
+
32
+
33
+ class MiniCPMClient:
34
+ """ๅฐ่ฃ… MiniCPM-o API ็š„ๅฎขๆˆท็ซฏ"""
35
+
36
+ def __init__(
37
+ self,
38
+ base_url: str = "http://localhost:8080",
39
+ timeout: int = 120,
40
+ default_max_tokens: int = 1024,
41
+ default_temperature: float = 0.7,
42
+ ):
43
+ self.base_url = base_url.rstrip("/")
44
+ self.timeout = timeout
45
+ self.default_max_tokens = default_max_tokens
46
+ self.default_temperature = default_temperature
47
+
48
+ def _post(self, path: str, body: dict) -> dict:
49
+ url = f"{self.base_url}{path}"
50
+ try:
51
+ r = requests.post(url, json=body, timeout=self.timeout)
52
+ r.raise_for_status()
53
+ return r.json()
54
+ except requests.exceptions.RequestException as e:
55
+ logger.error(f"API request failed: {e}")
56
+ raise
57
+
58
+ def health(self) -> dict:
59
+ """ๆฃ€ๆŸฅๆœๅŠกๅฅๅบท็Šถๆ€"""
60
+ r = requests.get(f"{self.base_url}/health", timeout=10)
61
+ return r.json()
62
+
63
+ # โ”€โ”€ ๆ–‡ๆœฌๅฏน่ฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
64
+
65
+ def chat(
66
+ self,
67
+ messages: list[dict],
68
+ system_prompt: Optional[str] = None,
69
+ max_tokens: Optional[int] = None,
70
+ temperature: Optional[float] = None,
71
+ stream: bool = False,
72
+ ) -> dict | str:
73
+ """
74
+ OpenAI ๅ…ผๅฎน็š„่ŠๅคฉๆŽฅๅฃ
75
+
76
+ Args:
77
+ messages: ๆถˆๆฏๅˆ—่กจ [{"role": "user", "content": "..."}]
78
+ system_prompt: ็ณป็ปŸๆ็คบ่ฏ
79
+ max_tokens: ๆœ€ๅคง็”Ÿๆˆ้•ฟๅบฆ
80
+ temperature: ๆธฉๅบฆๅ‚ๆ•ฐ
81
+ stream: ๆ˜ฏๅฆๆตๅผ
82
+
83
+ Returns:
84
+ dict ๆˆ–ๆตๅผๅญ—็ฌฆไธฒ
85
+ """
86
+ msgs = []
87
+ if system_prompt:
88
+ msgs.append({"role": "system", "content": system_prompt})
89
+ msgs.extend(messages)
90
+
91
+ body = {
92
+ "model": "MiniCPM-o-4_5",
93
+ "messages": msgs,
94
+ "max_tokens": max_tokens or self.default_max_tokens,
95
+ "temperature": temperature or self.default_temperature,
96
+ "stream": stream,
97
+ }
98
+ return self._post("/v1/chat/completions", body)
99
+
100
+ def ask(self, prompt: str, system_prompt: Optional[str] = None) -> str:
101
+ """็ฎ€ๅŒ–่ฐƒ็”จ๏ผšๅ‘้€้—ฎ้ข˜๏ผŒ่ฟ”ๅ›žๆ–‡ๆœฌๅ›ž็ญ”"""
102
+ result = self.chat(
103
+ messages=[{"role": "user", "content": prompt}],
104
+ system_prompt=system_prompt,
105
+ )
106
+ try:
107
+ return result["choices"][0]["message"]["content"]
108
+ except (KeyError, IndexError, TypeError):
109
+ logger.error(f"Unexpected response format: {result}")
110
+ return str(result)
111
+
112
+ # โ”€โ”€ ๅคšๆจกๆ€๏ผˆๅ›พ็‰‡็†่งฃ๏ผ‰ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
113
+
114
+ def chat_with_image(
115
+ self,
116
+ prompt: str,
117
+ image_base64: str,
118
+ image_format: str = "jpeg",
119
+ max_tokens: Optional[int] = None,
120
+ temperature: Optional[float] = None,
121
+ ) -> dict:
122
+ """
123
+ ๅ‘้€ๅ›พ็‰‡ + ๆ–‡ๅญ—่ฟ›่กŒๅคšๆจกๆ€ๅฏน่ฏ
124
+
125
+ Args:
126
+ prompt: ๆ–‡ๅญ—ๆ็คบ
127
+ image_base64: base64 ็ผ–็ ็š„ๅ›พ็‰‡
128
+ image_format: ๅ›พ็‰‡ๆ ผๅผ (jpeg, png, webp)
129
+ """
130
+ body = {
131
+ "messages": [
132
+ {
133
+ "role": "user",
134
+ "content": [
135
+ {"type": "text", "text": prompt},
136
+ {
137
+ "type": "image_url",
138
+ "image_url": {
139
+ "url": f"data:image/{image_format};base64,{image_base64}"
140
+ },
141
+ },
142
+ ],
143
+ }
144
+ ],
145
+ "max_tokens": max_tokens or self.default_max_tokens,
146
+ "temperature": temperature or self.default_temperature,
147
+ }
148
+ return self._post("/v1/multimodal/chat", body)
149
+
150
+ def describe_image(self, image_base64: str, image_format: str = "jpeg") -> str:
151
+ """็ฎ€ๅŒ–่ฐƒ็”จ๏ผšๆ่ฟฐๅ›พ็‰‡ๅ†…ๅฎน"""
152
+ result = self.chat_with_image(
153
+ prompt="่ฏท่ฏฆ็ป†ๆ่ฟฐ่ฟ™ๅผ ๅ›พ็‰‡ไธญ็š„ๅ†…ๅฎนใ€‚",
154
+ image_base64=image_base64,
155
+ image_format=image_format,
156
+ )
157
+ return result.get("response", str(result))
158
+
159
+ # โ”€โ”€ ๅตŒๅ…ฅๅ‘้‡ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
160
+
161
+ def embed(self, texts: list[str]) -> list[list[float]]:
162
+ """่Žทๅ–ๆ–‡ๆœฌ็š„ๅตŒๅ…ฅๅ‘้‡"""
163
+ body = {
164
+ "model": "MiniCPM-o-4_5",
165
+ "input": texts,
166
+ }
167
+ result = self._post("/v1/embeddings", body)
168
+ return [item["embedding"] for item in result.get("data", [])]
169
+
170
+
171
+ # โ”€โ”€ ้ข„่ฎพๆ็คบ่ฏ (้’ˆๅฏนๅญ•ๆœŸ้™ชๆŠคๅœบๆ™ฏ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
172
+
173
+ SYSTEM_PROMPTS = {
174
+ "diet_advisor": """ไฝ ๆ˜ฏไธ€ไฝไธ“ไธš็š„ๅญ•ๆœŸ่ฅๅ…ป้กพ้—ฎใ€‚่ฏทๆ นๆฎไปฅไธ‹ๅŽŸๅˆ™ๆไพ›ๅปบ่ฎฎ๏ผš
175
+ 1. ไผ˜ๅ…ˆ่€ƒ่™‘้ฃŸๆ็š„ๆ˜“ๅพ—ๆ€งๅ’Œๅฎถๅบญๅˆถไฝœ่ƒฝๅŠ›
176
+ 2. ๆณจๆ„ๅญ•ๆœŸๅ„้˜ถๆฎต็š„่ฅๅ…ป้œ€ๆฑ‚ๅทฎๅผ‚
177
+ 3. ้ฟๅ…็”Ÿๅ†ทใ€่พ›่พฃใ€้ซ˜ๆฑž้ฑผ็ฑป็ญ‰ๅญ•ๆœŸ็ฆๅฟŒ้ฃŸ็‰ฉ
178
+ 4. ๆŽจ่ๅฏŒๅซๅถ้…ธใ€้“ใ€้’™ใ€DHA ็š„้ฃŸ็‰ฉ
179
+ 5. ๅ›ž็ญ”็ฎ€ๆดๅฎž็”จ๏ผŒ็ป™ๅ‡บๅ…ทไฝ“ๅฏๆ“ไฝœ็š„่œๅ“ๅปบ่ฎฎ""",
180
+
181
+ "meal_planner": """ไฝ ๆ˜ฏไธ€ไฝๅฎถๅบญ่ฅๅ…ปๅธˆ๏ผŒไธ“้—จไธบๅญ•ๅฆ‡่ฎพ่ฎกๆ—ฅๅธธ่œๅ•ใ€‚่ฆๆฑ‚๏ผš
182
+ 1. ๆฏๅคฉ่ฎพ่ฎกๆ—ฉไธญๆ™šไธ‰้ค + 2ๆฌกๅŠ ้ค
183
+ 2. ็กฎไฟ่ฅๅ…ปๅ‡่กก๏ผŒ่ฆ†็›–่›‹็™ฝ่ดจใ€็ขณๆฐดใ€่„‚่‚ชใ€็ปด็”Ÿ็ด ใ€็Ÿฟ็‰ฉ่ดจ
184
+ 3. ่€ƒ่™‘ไธญๅ›ฝๅฎถๅบญ็š„็ƒน้ฅชไน ๆƒฏๅ’Œๅธธ่ง้ฃŸๆ
185
+ 4. ๆ ‡ๆณจๆฏ้ค็š„ๅ…ณ้”ฎ่ฅๅ…ป็ด 
186
+ 5. ็ป™ๅ‡บ็ฎ€่ฆ็š„ๅˆถไฝœๆญฅ้ชค""",
187
+
188
+ "food_analyzer": """ๅˆ†ๆž็”จๆˆทๆไพ›็š„้ฃŸ็‰ฉ/่œๅ“ๆ่ฟฐ๏ผŒๆๅ–ไปฅไธ‹ไฟกๆฏ๏ผš
189
+ 1. ่œๅ
190
+ 2. ไธป่ฆ้ฃŸๆ
191
+ 3. ่ฅๅ…ปๆˆๅˆ†่ฏ„ไผฐ๏ผˆ็ƒญ้‡ใ€่›‹็™ฝ่ดจใ€่„‚่‚ชใ€็ขณๆฐดใ€ๅ…ณ้”ฎๅพฎ้‡ๅ…ƒ็ด ๏ผ‰
192
+ 4. ๅญ•ๆœŸ้€‚ๅฎœๅบฆ๏ผˆๆŽจ่/ๅฏ้ฃŸ็”จ/ๆ…Ž้ฃŸ/ๅฟŒ้ฃŸ๏ผ‰
193
+ 5. ๅปบ่ฎฎ๏ผˆๅฆ‚ๆœ‰๏ผ‰
194
+ ่ฏท็”จ JSON ๆ ผๅผๅ›žๅคใ€‚""",
195
+
196
+ "nutrition_summary": """ๆ นๆฎ็”จๆˆทๆไพ›็š„้ฅฎ้ฃŸ่ฎฐๅฝ•๏ผŒๆ€ป็ป“่ฅๅ…ปๆ‘„ๅ…ฅๆƒ…ๅ†ต๏ผš
197
+ 1. ๅ„ๅคง็ฑป่ฅๅ…ป็ด ๆ‘„ๅ…ฅ่ฏ„ไผฐ
198
+ 2. ไธŽๅญ•ๆœŸๆŽจ่ๆ ‡ๅ‡†็š„ๅฏนๆฏ”
199
+ 3. ้œ€่ฆ่กฅๅ……ๆˆ–่ฐƒๆ•ด็š„ๆ–นๅ‘
200
+ 4. ๅ…ทไฝ“็š„ไธ€ๅ‘จ้ฅฎ้ฃŸๆ”นๅ–„ๅปบ่ฎฎ
201
+ ่ฏท็”จ็ป“ๆž„ๅŒ–ๆ ผๅผ่พ“ๅ‡บ๏ผŒไพฟไบŽๅ‰็ซฏๅฏ่ง†ๅŒ–""",
202
+ }
modal_deploy/deploy.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modal deployment for MiniCPM-o-4_5 via llama.cpp server
3
+ Serves OpenAI-compatible chat API + multimodal (text/image/audio) endpoints
4
+ """
5
+ import os
6
+ import sys
7
+ import subprocess
8
+ import modal
9
+ from modal import Image, App, Volume, Secret, asgi_app
10
+
11
+ # โ”€โ”€ constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+ MODEL_DIR = "/models"
13
+ MODEL_NAME = "MiniCPM-o-4_5"
14
+ # Model files expected in the volume
15
+ MMPROJ_VISION = f"{MODEL_DIR}/vision/MiniCPM-o-4_5-vision-F16.gguf"
16
+ MMPROJ_AUDIO = f"{MODEL_DIR}/audio/MiniCPM-o-4_5-audio-F16.gguf"
17
+ MMPROJ_TTS = f"{MODEL_DIR}/tts/MiniCPM-o-4_5-tts-F16.gguf"
18
+ MMPROJ_PROJ = f"{MODEL_DIR}/tts/MiniCPM-o-4_5-projector-F16.gguf"
19
+ MAIN_MODEL = f"{MODEL_DIR}/MiniCPM-o-4_5-Q4_K_M.gguf"
20
+
21
+ # Image with llama.cpp compiled
22
+ llamacpp_image = (
23
+ Image.debian_slim(python_version="3.11")
24
+ .apt_install("curl", "git", "build-essential", "cmake", "libcurl4-openssl-dev")
25
+ .pip_install("fastapi", "uvicorn", "httpx", "numpy", "Pillow", "soundfile")
26
+ .run_commands(
27
+ "git clone --depth 1 https://github.com/ggerganov/llama.cpp /llama.cpp",
28
+ "cd /llama.cpp && cmake -B build -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=OFF -DLLAMA_CURL=ON -DLLAMA_BUILD_SERVER=ON -DLLAMA_BUILD_TESTS=OFF -DLLAMA_BUILD_EXAMPLES=OFF",
29
+ "cd /llama.cpp && cmake --build build --config Release -j $(nproc) --target llama-server llama-mtmd-cli",
30
+ )
31
+ )
32
+
33
+ # Volume to store models (persisted across restarts)
34
+ model_volume = Volume.from_name("minicpm-o-4_5-models", create_if_missing=True)
35
+
36
+ app = App("prego-pal-minicpm")
37
+
38
+ # โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
39
+
40
+ def find_mmproj_files(model_dir: str) -> list[str]:
41
+ """Locate mmproj GGUF files in model directory structure."""
42
+ mmproj = []
43
+ for root, _, files in os.walk(model_dir):
44
+ for f in files:
45
+ if f.endswith(".gguf") and "mmproj" in f.lower():
46
+ mmproj.append(os.path.join(root, f))
47
+ return sorted(mmproj)
48
+
49
+
50
+ def build_server_args(model_dir: str, port: int = 8080) -> list[str]:
51
+ """Construct llama-server command line arguments."""
52
+ args = [
53
+ "/llama.cpp/build/bin/llama-server",
54
+ "-m", os.path.join(model_dir, "MiniCPM-o-4_5-Q4_K_M.gguf"),
55
+ "--host", "0.0.0.0",
56
+ "--port", str(port),
57
+ "-ngl", "99", # offload all layers to GPU if available
58
+ "-c", "8192", # context size
59
+ "--mlock", # prevent model from being swapped
60
+ "--no-mmap", # use standard I/O (better on Modal's tmpfs)
61
+ ]
62
+
63
+ # Attach mmproj files for multimodal support
64
+ mmproj_files = find_mmproj_files(model_dir)
65
+ for mf in mmproj_files:
66
+ args.extend(["--mmproj", mf])
67
+
68
+ return args
69
+
70
+
71
+ # โ”€โ”€ Entrypoint / API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
72
+
73
+ @app.function(
74
+ image=llamacpp_image,
75
+ volumes={MODEL_DIR: model_volume},
76
+ container_idle_timeout=300, # shutdown after 5min idle
77
+ allow_concurrent_inputs=10,
78
+ gpu="A100", # MiniCPM-o-4_5 needs GPU
79
+ timeout=600,
80
+ )
81
+ @asgi_app()
82
+ def serve():
83
+ """FastAPI app wrapping llama-server for OpenAI-compatible endpoints."""
84
+ import asyncio
85
+ import httpx
86
+ from fastapi import FastAPI, Request
87
+ from fastapi.responses import StreamingResponse, JSONResponse
88
+ from fastapi.middleware.cors import CORSMiddleware
89
+
90
+ web_app = FastAPI(title="PregoPal MiniCPM-o API")
91
+
92
+ web_app.add_middleware(
93
+ CORSMiddleware,
94
+ allow_origins=["*"],
95
+ allow_credentials=True,
96
+ allow_methods=["*"],
97
+ allow_headers=["*"],
98
+ )
99
+
100
+ # Start llama-server in background
101
+ server_port = 8080
102
+ server_args = build_server_args(MODEL_DIR, server_port)
103
+
104
+ print(f"[PregoPal] Starting llama-server: {' '.join(server_args)}")
105
+ proc = subprocess.Popen(
106
+ server_args,
107
+ stdout=subprocess.PIPE,
108
+ stderr=subprocess.STDOUT,
109
+ text=True,
110
+ )
111
+
112
+ llama_url = f"http://127.0.0.1:{server_port}"
113
+
114
+ # Wait for server to be ready
115
+ async def wait_for_server(timeout: float = 120.0):
116
+ async with httpx.AsyncClient() as client:
117
+ start = asyncio.get_event_loop().time()
118
+ while True:
119
+ try:
120
+ r = await client.get(f"{llama_url}/health", timeout=5)
121
+ if r.status_code == 200:
122
+ print("[PregoPal] llama-server ready!")
123
+ return
124
+ except Exception:
125
+ pass
126
+ if asyncio.get_event_loop().time() - start > timeout:
127
+ raise RuntimeError("llama-server startup timed out")
128
+ await asyncio.sleep(1)
129
+
130
+ @web_app.on_event("startup")
131
+ async def startup():
132
+ await wait_for_server()
133
+
134
+ @web_app.on_event("shutdown")
135
+ async def shutdown():
136
+ proc.terminate()
137
+ try:
138
+ proc.wait(timeout=10)
139
+ except subprocess.TimeoutExpired:
140
+ proc.kill()
141
+
142
+ # โ”€โ”€ Proxy endpoints to llama-server โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
143
+
144
+ @web_app.post("/v1/chat/completions")
145
+ async def chat_completions(request: Request):
146
+ """OpenAI-compatible chat completions (text)."""
147
+ body = await request.json()
148
+ async with httpx.AsyncClient(timeout=300) as client:
149
+ r = await client.post(f"{llama_url}/v1/chat/completions", json=body)
150
+ # Stream if requested
151
+ if body.get("stream", False):
152
+ return StreamingResponse(
153
+ r.aiter_bytes(),
154
+ media_type="text/event-stream",
155
+ headers=dict(r.headers),
156
+ )
157
+ return JSONResponse(r.json(), status_code=r.status_code)
158
+
159
+ @web_app.post("/v1/embeddings")
160
+ async def embeddings(request: Request):
161
+ """OpenAI-compatible embeddings."""
162
+ body = await request.json()
163
+ async with httpx.AsyncClient(timeout=120) as client:
164
+ r = await client.post(f"{llama_url}/v1/embeddings", json=body)
165
+ return JSONResponse(r.json(), status_code=r.status_code)
166
+
167
+ @web_app.get("/health")
168
+ async def health():
169
+ async with httpx.AsyncClient() as client:
170
+ r = await client.get(f"{llama_url}/health")
171
+ return JSONResponse(r.json(), status_code=r.status_code)
172
+
173
+ @web_app.get("/v1/models")
174
+ async def list_models():
175
+ async with httpx.AsyncClient() as client:
176
+ r = await client.get(f"{llama_url}/v1/models")
177
+ return JSONResponse(r.json(), status_code=r.status_code)
178
+
179
+ # โ”€โ”€ Multimodal endpoint โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
180
+
181
+ @web_app.post("/v1/multimodal/chat")
182
+ async def multimodal_chat(request: Request):
183
+ """
184
+ Custom multimodal chat endpoint.
185
+
186
+ Request format:
187
+ {
188
+ "messages": [
189
+ {"role": "user", "content": [
190
+ {"type": "text", "text": "Describe this image"},
191
+ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}}
192
+ ]}
193
+ ],
194
+ "max_tokens": 1024,
195
+ "temperature": 0.7
196
+ }
197
+ """
198
+ import base64
199
+ import tempfile
200
+ import json as json_mod
201
+
202
+ body = await request.json()
203
+ messages = body.get("messages", [])
204
+ max_tokens = body.get("max_tokens", 1024)
205
+ temperature = body.get("temperature", 0.7)
206
+
207
+ # Extract text prompt and image
208
+ prompt_parts = []
209
+ image_path = None
210
+
211
+ for msg in messages:
212
+ if msg["role"] == "user":
213
+ content = msg["content"]
214
+ if isinstance(content, str):
215
+ prompt_parts.append(content)
216
+ elif isinstance(content, list):
217
+ for item in content:
218
+ if item["type"] == "text":
219
+ prompt_parts.append(item["text"])
220
+ elif item["type"] == "image_url":
221
+ data_url = item["image_url"]["url"]
222
+ if data_url.startswith("data:image/"):
223
+ # Parse base64 image
224
+ header, b64 = data_url.split(",", 1)
225
+ fmt = header.split("/")[1].split(";")[0]
226
+ img_data = base64.b64decode(b64)
227
+ fd, image_path = tempfile.mkstemp(suffix=f".{fmt}")
228
+ os.close(fd)
229
+ with open(image_path, "wb") as f:
230
+ f.write(img_data)
231
+
232
+ prompt = "\n".join(prompt_parts)
233
+
234
+ # Build llama-mtmd-cli command with JSON output
235
+ cmd = [
236
+ "/llama.cpp/build/bin/llama-mtmd-cli",
237
+ "-m", MAIN_MODEL,
238
+ "--mmproj", MMPROJ_VISION,
239
+ "-ngl", "99",
240
+ "-c", "8192",
241
+ "-n", str(max_tokens),
242
+ "--temp", str(temperature),
243
+ "--json-schema", json_mod.dumps({
244
+ "type": "object",
245
+ "properties": {
246
+ "response": {"type": "string"}
247
+ }
248
+ }),
249
+ "-p", prompt,
250
+ ]
251
+
252
+ if image_path:
253
+ cmd.extend(["--image", image_path])
254
+
255
+ try:
256
+ proc = await asyncio.create_subprocess_exec(
257
+ *cmd,
258
+ stdout=asyncio.subprocess.PIPE,
259
+ stderr=asyncio.subprocess.PIPE,
260
+ )
261
+ stdout, stderr = await asyncio.wait_for(
262
+ proc.communicate(), timeout=120
263
+ )
264
+ if image_path:
265
+ os.unlink(image_path)
266
+
267
+ output = stdout.decode("utf-8", errors="replace").strip()
268
+ try:
269
+ result = json_mod.loads(output)
270
+ return JSONResponse(result)
271
+ except json_mod.JSONDecodeError:
272
+ return JSONResponse({"response": output, "raw": True})
273
+
274
+ except asyncio.TimeoutError:
275
+ if image_path and os.path.exists(image_path):
276
+ os.unlink(image_path)
277
+ return JSONResponse(
278
+ {"error": "Request timed out"}, status_code=504
279
+ )
280
+ except Exception as e:
281
+ if image_path and os.path.exists(image_path):
282
+ os.unlink(image_path)
283
+ return JSONResponse({"error": str(e)}, status_code=500)
284
+
285
+ @web_app.get("/")
286
+ async def root():
287
+ return {"service": "PregoPal MiniCPM-o-4_5 API", "version": "1.0.0"}
288
+
289
+ return web_app
290
+
291
+
292
+ # โ”€โ”€ Deploy helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
293
+
294
+ @app.function(
295
+ image=llamacpp_image,
296
+ volumes={MODEL_DIR: model_volume},
297
+ gpu="A100",
298
+ timeout=3600,
299
+ )
300
+ def upload_models():
301
+ """One-time function to upload model files to Modal Volume.
302
+
303
+ Run: modal run modal_deploy/deploy.py::upload_models
304
+ Requires models in ../models/MiniCPM-o-4_5-gguf/
305
+ """
306
+ import shutil
307
+ local_dir = "/local_models"
308
+
309
+ # This function assumes models are mounted locally
310
+ # On Modal, you'd use modal volume put instead
311
+ print("Model upload placeholder - use 'modal volume put' CLI")
312
+ print(f" modal volume put minicpm-o-4_5-models ../models/MiniCPM-o-4_5-gguf /")
313
+
314
+
315
+ # โ”€โ”€ Health check function โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
316
+
317
+ @app.function(
318
+ image=llamacpp_image,
319
+ volumes={MODEL_DIR: model_volume},
320
+ gpu="A100",
321
+ timeout=300,
322
+ )
323
+ def test_inference():
324
+ """Quick test to verify model loads and runs.
325
+
326
+ Run: modal run modal_deploy/deploy.py::test_inference
327
+ """
328
+ import httpx
329
+ import time
330
+
331
+ port = 8081
332
+ args = build_server_args(MODEL_DIR, port)
333
+
334
+ print(f"Starting server: {' '.join(args)}")
335
+ proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
336
+
337
+ url = f"http://127.0.0.1:{port}"
338
+ deadline = time.time() + 120
339
+ while time.time() < deadline:
340
+ try:
341
+ r = httpx.get(f"{url}/health", timeout=5)
342
+ if r.status_code == 200:
343
+ print("Server healthy!")
344
+ break
345
+ except Exception:
346
+ pass
347
+ time.sleep(2)
348
+ else:
349
+ proc.terminate()
350
+ raise RuntimeError("Server failed to start")
351
+
352
+ # Test completion
353
+ r = httpx.post(
354
+ f"{url}/v1/chat/completions",
355
+ json={
356
+ "model": "MiniCPM-o-4_5",
357
+ "messages": [
358
+ {"role": "user", "content": "Say hello in Chinese"}
359
+ ],
360
+ "max_tokens": 50,
361
+ "temperature": 0.1,
362
+ },
363
+ timeout=120,
364
+ )
365
+ print(f"Response: {r.json()}")
366
+ proc.terminate()
requirements.txt CHANGED
@@ -2,6 +2,7 @@
2
  # ๅฎ‰่ฃ…: pip install -r requirements.txt
3
 
4
  gradio>=6.0.0
 
5
  numpy>=1.24.0
6
  matplotlib>=3.7.0
7
  pandas>=2.0.0
 
2
  # ๅฎ‰่ฃ…: pip install -r requirements.txt
3
 
4
  gradio>=6.0.0
5
+ modal>=0.60.0
6
  numpy>=1.24.0
7
  matplotlib>=3.7.0
8
  pandas>=2.0.0