Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mllm-cli/cmd/mllm-client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,9 @@ func main() {
history = history[:len(history)-1]
continue
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body.Close()
log.Printf("ERROR: Server returned status %s: %s", resp.Status, string(bodyBytes))
history = history[:len(history)-1]
continue
Expand Down Expand Up @@ -83,6 +82,7 @@ func main() {
}
fmt.Println()
if err := scanner.Err(); err != nil { log.Printf("ERROR reading stream: %v", err) }
resp.Body.Close()
history = append(history, api.RequestMessage{Role: "assistant", Content: fullResponse.String()})
}
}
14 changes: 12 additions & 2 deletions mllm-cli/cmd/mllm-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

func main() {
modelPath := flag.String("model-path", "", "Path to the MLLM model directory.")
probePath := flag.String("probe-path", "", "Path to the probes directory for Qwen3 probing session.")
ocrModelPath := flag.String("ocr-model-path", "", "Path to the DeepSeek-OCR model directory.")
flag.Parse()

Expand All @@ -35,7 +36,16 @@ func main() {

if *modelPath != "" {
log.Printf("Loading Qwen3 model and creating session from: %s", *modelPath)
session, err := mllm.NewSession(*modelPath)
var (
session *mllm.Session
err error
)
if *probePath != "" {
log.Printf("Probing enabled. Loading probes from: %s", *probePath)
session, err = mllm.NewProbingSession(*modelPath, *probePath)
} else {
session, err = mllm.NewSession(*modelPath)
}
if err != nil {
log.Fatalf("FATAL: Failed to create Qwen3 session: %v", err)
}
Expand Down Expand Up @@ -89,4 +99,4 @@ func main() {
mllmService.Shutdown()

log.Println("Server gracefully stopped.")
}
}
16 changes: 11 additions & 5 deletions mllm-cli/go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
module mllm-cli

go 1.23.0
go 1.25.0

toolchain go1.24.11
require (
github.com/charmbracelet/bubbles v0.21.0
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4
)

require github.com/charmbracelet/bubbles v0.21.0
require (
golang.org/x/mod v0.33.0 // indirect
golang.org/x/tools v0.42.0 // indirect
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
Expand All @@ -27,8 +33,8 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.34.0
golang.org/x/text v0.3.8 // indirect
)
16 changes: 12 additions & 4 deletions mllm-cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
Expand Down Expand Up @@ -49,13 +51,19 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4 h1:uT3oYo9M38vJa7JpT4kCie2lJwOpoUrx7FvV0H7kXSc=
golang.org/x/mobile v0.0.0-20260217195705-b56b3793a9c4/go.mod h1:4OGHIUSBiIqyFAQDaX1tpY0BVnO20DvNDeATBu8aeFQ=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
166 changes: 92 additions & 74 deletions mllm-cli/mllm/c.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ import "unsafe"
import "fmt"
import "runtime"


type Session struct {
cHandle C.MllmCAny
sessionID string
cHandle C.MllmCAny
sessionID string
}

func isOk(any C.MllmCAny) bool {
Expand All @@ -43,103 +42,122 @@ func ShutdownContext() bool {
}

func StartService(workerThreads int) bool {
result := C.startService(C.size_t(workerThreads))
return isOk(result)
result := C.startService(C.size_t(workerThreads))
return isOk(result)
}
Comment on lines 44 to 47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether callers can pass unchecked/derived values into StartService.
rg -nP --type=go -C3 '\bStartService\s*\('
rg -nP --type=go -C3 '\bworkerThreads\b'

Repository: UbiquitousLearning/mllm

Length of output: 1586


Add defensive check for negative workerThreads parameter.

The function parameter accepts int, which can be negative. While all current callers (in main.go and mobile_server.go) pass the literal value 1, a defensive check would prevent accidental misuse if the API is called with untrusted input in the future.

💡 Suggested improvement
 func StartService(workerThreads int) bool {
+	if workerThreads <= 0 {
+		return false
+	}
 	result := C.startService(C.size_t(workerThreads))
 	return isOk(result)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func StartService(workerThreads int) bool {
result := C.startService(C.size_t(workerThreads))
return isOk(result)
result := C.startService(C.size_t(workerThreads))
return isOk(result)
}
func StartService(workerThreads int) bool {
if workerThreads <= 0 {
return false
}
result := C.startService(C.size_t(workerThreads))
return isOk(result)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mllm-cli/mllm/c.go` around lines 44 - 47, StartService currently passes an
int workerThreads directly to C.startService allowing negatives; add a defensive
check at the top of StartService to clamp or reject negative values (e.g., if
workerThreads < 0 set to 0 or return false) before calling C.startService, then
call C.startService with the sanitized value and return isOk(result); update
references to workerThreads in StartService and ensure the conversion
C.size_t(...) uses the sanitized variable.


func StopService() bool {
result := C.stopService()
return isOk(result)
result := C.stopService()
return isOk(result)
}

func SetLogLevel(level int) {
C.setLogLevel(C.int(level))
C.setLogLevel(C.int(level))
}

func NewSession(modelPath string) (*Session, error) {
cModelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cModelPath))
cModelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cModelPath))

handle := C.createQwen3Session(cModelPath)
if !isOk(handle) {
return nil, fmt.Errorf("底层C API createQwen3Session 失败")
}
s := &Session{cHandle: handle}
runtime.SetFinalizer(s, func(s *Session) {
fmt.Println("[Go Finalizer] Mllm Session automatically released.")
C.freeSession(s.cHandle)
})

return s, nil
}

func NewProbingSession(modelPath string, probePath string) (*Session, error) {
cModelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cModelPath))
cProbePath := C.CString(probePath)
defer C.free(unsafe.Pointer(cProbePath))

handle := C.createQwen3Session(cModelPath)
if !isOk(handle) {
return nil, fmt.Errorf("底层C API createQwen3Session 失败")
}
s := &Session{cHandle: handle}
runtime.SetFinalizer(s, func(s *Session) {
fmt.Println("[Go Finalizer] Mllm Session automatically released.")
C.freeSession(s.cHandle)
})
handle := C.createQwen3ProbingSession(cModelPath, cProbePath)
if !isOk(handle) {
return nil, fmt.Errorf("底层C API createQwen3ProbingSession 失败")
}
s := &Session{cHandle: handle}
runtime.SetFinalizer(s, func(s *Session) {
fmt.Println("[Go Finalizer] Mllm Probing Session automatically released.")
C.freeSession(s.cHandle)
})

return s, nil
return s, nil
}

func NewDeepseekOCRSession(modelPath string) (*Session, error) {
cModelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cModelPath))
cModelPath := C.CString(modelPath)
defer C.free(unsafe.Pointer(cModelPath))

handle := C.createDeepseekOCRSession(cModelPath)
if !isOk(handle) {
return nil, fmt.Errorf("底层C API createDeepseekOCRSession 失败")
}
s := &Session{cHandle: handle}
runtime.SetFinalizer(s, func(s *Session) {
fmt.Println("[Go Finalizer] Mllm OCR Session automatically released.")
C.freeSession(s.cHandle)
})
handle := C.createDeepseekOCRSession(cModelPath)
if !isOk(handle) {
return nil, fmt.Errorf("底层C API createDeepseekOCRSession 失败")
}
s := &Session{cHandle: handle}
runtime.SetFinalizer(s, func(s *Session) {
fmt.Println("[Go Finalizer] Mllm OCR Session automatically released.")
C.freeSession(s.cHandle)
})

return s, nil
return s, nil
}

func (s *Session) Close() {
if C.MllmCAny_get_v_custom_ptr(s.cHandle) != nil {
fmt.Println("[Go Close] Mllm Session manually closed.")
C.freeSession(s.cHandle)
s.cHandle = C.MllmCAny_set_v_custom_ptr_null(s.cHandle)
runtime.SetFinalizer(s, nil)
}
if C.MllmCAny_get_v_custom_ptr(s.cHandle) != nil {
fmt.Println("[Go Close] Mllm Session manually closed.")
C.freeSession(s.cHandle)
s.cHandle = C.MllmCAny_set_v_custom_ptr_null(s.cHandle)
runtime.SetFinalizer(s, nil)
}
Comment on lines 111 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the Close() function
find . -name "c.go" -path "*mllm-cli*" | head -5

Repository: UbiquitousLearning/mllm

Length of output: 87


🏁 Script executed:

# Read the specific lines from the file
sed -n '105,120p' ./mllm-cli/mllm/c.go

Repository: UbiquitousLearning/mllm

Length of output: 426


🏁 Script executed:

# Check if Close() is used in deferred/cleanup contexts
rg "defer.*\.Close\(\)" ./mllm-cli --type go -A 2 -B 2 | head -30

Repository: UbiquitousLearning/mllm

Length of output: 291


🏁 Script executed:

# Search for Session.Close() usage patterns
rg "Session.*Close\(\)" ./mllm-cli --type go -B 3 -A 2 | head -50

Repository: UbiquitousLearning/mllm

Length of output: 348


🏁 Script executed:

# Check where Close() is actually called or if it's used as a callback
rg "\.Close" ./mllm-cli/mllm --type go -B 2 -A 2

Repository: UbiquitousLearning/mllm

Length of output: 49


🏁 Script executed:

# Broader search for Close usage in mllm-cli
rg "Close" ./mllm-cli --type go | head -20

Repository: UbiquitousLearning/mllm

Length of output: 738


🏁 Script executed:

# Check the actual calls to session.Close() in the service and main files
rg -B 5 -A 2 "session\.Close\(\)" ./mllm-cli --type go

Repository: UbiquitousLearning/mllm

Length of output: 1612


Add nil-safety check to Close() method to prevent panic on nil receiver.

The function dereferences s.cHandle immediately without checking if the receiver is nil. While current code paths pass non-nil receivers, calling Close() on a nil *Session will panic. This is a defensive Go pattern that should be adopted:

Proposed fix
 func (s *Session) Close() {
+	if s == nil {
+		return
+	}
 	if C.MllmCAny_get_v_custom_ptr(s.cHandle) != nil {
 		fmt.Println("[Go Close] Mllm Session manually closed.")
 		C.freeSession(s.cHandle)
 		s.cHandle = C.MllmCAny_set_v_custom_ptr_null(s.cHandle)
 		runtime.SetFinalizer(s, nil)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mllm-cli/mllm/c.go` around lines 111 - 117, The Close method on *Session
dereferences the receiver without nil-checking which can panic if Close is
called on a nil *Session; add an early guard at the top of Session.Close that
returns immediately if s == nil (and also treat s.cHandle == nil as a no-op)
before calling C.MllmCAny_get_v_custom_ptr, C.freeSession or
runtime.SetFinalizer to make Close nil-safe while keeping the existing cleanup
logic intact.

}

func (s *Session) Insert(sessionID string) bool {
cSessionID := C.CString(sessionID)
defer C.free(unsafe.Pointer(cSessionID))
result := C.insertSession(cSessionID, s.cHandle)
if isOk(result) {
s.sessionID = sessionID
}
return isOk(result)
cSessionID := C.CString(sessionID)
defer C.free(unsafe.Pointer(cSessionID))
result := C.insertSession(cSessionID, s.cHandle)
if isOk(result) {
s.sessionID = sessionID
}
return isOk(result)
}

func (s *Session) SendRequest(jsonRequest string) bool {
if s.sessionID == "" {
fmt.Println("[Go SendRequest] Error: sessionID is not set on this session.")
return false
}
cSessionID := C.CString(s.sessionID)
cJsonRequest := C.CString(jsonRequest)
defer C.free(unsafe.Pointer(cSessionID))
defer C.free(unsafe.Pointer(cJsonRequest))

result := C.sendRequest(cSessionID, cJsonRequest)
return isOk(result)
}

func (s *Session) PollResponse(requestID string) string {
if requestID == "" {
fmt.Println("[Go PollResponse] Error: requestID cannot be empty.")
return ""
}
cRequestID := C.CString(requestID)
defer C.free(unsafe.Pointer(cRequestID))

cResponse := C.pollResponse(cRequestID)
if cResponse == nil {
return ""
}
defer C.freeResponseString(cResponse)
return C.GoString(cResponse)
if s.sessionID == "" {
fmt.Println("[Go SendRequest] Error: sessionID is not set on this session.")
return false
}
cSessionID := C.CString(s.sessionID)
cJsonRequest := C.CString(jsonRequest)
defer C.free(unsafe.Pointer(cSessionID))
defer C.free(unsafe.Pointer(cJsonRequest))

result := C.sendRequest(cSessionID, cJsonRequest)
return isOk(result)
}

func (s *Session) PollResponse(requestID string) string {
if requestID == "" {
fmt.Println("[Go PollResponse] Error: requestID cannot be empty.")
return ""
}
cRequestID := C.CString(requestID)
defer C.free(unsafe.Pointer(cRequestID))

cResponse := C.pollResponse(cRequestID)
if cResponse == nil {
return ""
}
defer C.freeResponseString(cResponse)

return C.GoString(cResponse)
}

func (s *Session) SessionID() string {
return s.sessionID
return s.sessionID
}
Loading
Loading