Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Copy Markdown
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
Copy Markdown
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