Skip to content
Closed
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
1 change: 1 addition & 0 deletions apps/Android/MnnLlmChat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ This is our full multimodal language model (LLM) Android app
+ Bugfix:
+ Fix the Android chat regression that could stop after a single token or fail to continue on the second turn with the prebuilt runtime.
+ Harden dumpapp and smoke coverage for terminal callback delivery, single-token regression, and thinking-mode divergence checks.
+ Reuse the loaded runtime session when starting API service for the same model to avoid extra reloads that could freeze or crash API startup.

## Version 0.8.2
+ Click here to [download](https://meta.alicdn.com/data/mnn/apks/mnn_chat_0_8_2.apk)
Expand Down
1 change: 1 addition & 0 deletions apps/Android/MnnLlmChat/README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
+ 问题修复:
+ 修复 Android 聊天在预编译 runtime 下可能只输出单个 token,或第二轮无法继续生成的问题。
+ 加强 dumpapp 与 smoke 对终止回调、单 token 回归和思考模式差异判定的覆盖。
+ 对同模型启动 API 服务时复用已加载 runtime session,避免额外重载导致的卡死或崩溃。

## Version 0.8.2
+ 点击这里 [下载](https://meta.alicdn.com/data/mnn/apks/mnn_chat_0_8_2.apk)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.alibaba.mnnllm.api.openai.service

internal object ApiRuntimeSessionStartPolicy {
fun shouldReuseLoadedSession(
requestedModelId: String?,
activeModelId: String?,
hasLoadedActiveSession: Boolean
): Boolean {
if (!hasLoadedActiveSession) {
return false
}
return requestedModelId.isNullOrBlank() || requestedModelId == activeModelId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ApiServiceCoordinator(private val context: Context) {

if (application != null && _isServerRunning) {
if (!modelId.isNullOrBlank()) {
val switchedSession = ensureRuntimeSessionForModel(modelId)
val switchedSession = resolveRuntimeSessionForStart(modelId)
if (switchedSession == null) {
notificationManager?.updateNotification(
context.getString(R.string.api_service_not_started),
Expand All @@ -71,21 +71,7 @@ class ApiServiceCoordinator(private val context: Context) {
}

return runCatching {
val runtimeSession = if (!modelId.isNullOrBlank()) {
val session = ensureRuntimeSessionForModel(modelId)
if (session == null) {
notificationManager?.updateNotification(
context.getString(R.string.api_service_not_started),
context.getString(R.string.no_active_session)
)
return false
}
session
} else {
ServiceLocator.getLlmRuntimeController().getActiveSession()
}

val session = runtimeSession ?: ServiceLocator.getChatSessionProvider().getLlmSession()
val session = resolveRuntimeSessionForStart(modelId)
if (session == null) {
Timber.Forest.tag(TAG).w("No active LlmSession found")
notificationManager?.updateNotification(
Expand Down Expand Up @@ -125,6 +111,31 @@ class ApiServiceCoordinator(private val context: Context) {
}
}

private fun resolveRuntimeSessionForStart(modelId: String?): LlmSession? {
val runtime = ServiceLocator.getLlmRuntimeController()
val chatSessionProvider = ServiceLocator.getChatSessionProvider()
val activeModelId = runtime.getActiveModelId()
val hasLoadedActiveSession = chatSessionProvider.hasActiveSession()

if (ApiRuntimeSessionStartPolicy.shouldReuseLoadedSession(modelId, activeModelId, hasLoadedActiveSession)) {
val activeSession = runtime.getActiveSession() ?: chatSessionProvider.getLlmSession()
if (activeSession != null) {
Timber.Forest.tag(TAG).i(
"Reusing loaded runtime session for API start, requestedModelId=%s activeModelId=%s",
modelId,
activeModelId
)
return activeSession
}
}

if (!modelId.isNullOrBlank()) {
return ensureRuntimeSessionForModel(modelId)
}

return runtime.getActiveSession() ?: chatSessionProvider.getLlmSession()
}

private fun ensureRuntimeSessionForModel(modelId: String): LlmSession? {
val ensureResult = ServiceLocator.getLlmRuntimeController().ensureSession(modelId)
if (!ensureResult.success || ensureResult.session == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.alibaba.mnnllm.api.openai.service

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class ApiRuntimeSessionStartPolicyTest {

@Test
fun `same model with loaded active session should reuse runtime session`() {
assertTrue(
ApiRuntimeSessionStartPolicy.shouldReuseLoadedSession(
requestedModelId = "ModelScope/MNN/Qwen3.5-0.8B-MNN",
activeModelId = "ModelScope/MNN/Qwen3.5-0.8B-MNN",
hasLoadedActiveSession = true
)
)
}

@Test
fun `different model should not reuse runtime session`() {
assertFalse(
ApiRuntimeSessionStartPolicy.shouldReuseLoadedSession(
requestedModelId = "ModelScope/MNN/Qwen3.5-2B-MNN",
activeModelId = "ModelScope/MNN/Qwen3.5-0.8B-MNN",
hasLoadedActiveSession = true
)
)
}

@Test
fun `missing loaded session should not reuse runtime session`() {
assertFalse(
ApiRuntimeSessionStartPolicy.shouldReuseLoadedSession(
requestedModelId = "ModelScope/MNN/Qwen3.5-0.8B-MNN",
activeModelId = "ModelScope/MNN/Qwen3.5-0.8B-MNN",
hasLoadedActiveSession = false
)
)
}
}
Loading