summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-09 21:52:16 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-09 21:52:16 +0900
commit1729e1c399a41b4128b5dd7ef0286fae128e6157 (patch)
treead1b8f62368419c692bd140e0005ca9c675fc349
parent1a56663476de9ff42d70ef68c868de09d8b2d5ff (diff)
ollama側のレスポンスが空の場合に1回リトライする処理に変更
トークン数の累積計算処理削除
-rw-r--r--src/ollama_web/i18n/messages/en.json4
-rw-r--r--src/ollama_web/i18n/messages/ja.json4
-rw-r--r--src/ollama_web/llm.py39
-rw-r--r--src/ollama_web/sessions.py30
-rw-r--r--src/ollama_web/static/app.js11
-rw-r--r--tests/test_clear_messages.py1
6 files changed, 74 insertions, 15 deletions
diff --git a/src/ollama_web/i18n/messages/en.json b/src/ollama_web/i18n/messages/en.json
index 9e2894a..42bce33 100644
--- a/src/ollama_web/i18n/messages/en.json
+++ b/src/ollama_web/i18n/messages/en.json
@@ -78,7 +78,9 @@
"tool_limit_reached": "Tool call limit reached. Summarizing findings…",
"thinking": "Thinking process",
"executing_tool": "Executing {label}…",
- "running": "Running…"
+ "running": "Running…",
+ "empty_response": "The model returned an empty response. Please send the message again.",
+ "empty_response_retry": "Empty response received; retrying…"
},
"errors": {
"generic": "An error occurred",
diff --git a/src/ollama_web/i18n/messages/ja.json b/src/ollama_web/i18n/messages/ja.json
index aa9f01e..a476ca2 100644
--- a/src/ollama_web/i18n/messages/ja.json
+++ b/src/ollama_web/i18n/messages/ja.json
@@ -78,7 +78,9 @@
"tool_limit_reached": "ツール呼び出し上限に達しました。これまでの調査結果を整理中…",
"thinking": "思考プロセス",
"executing_tool": "{label} を実行中…",
- "running": "実行中…"
+ "running": "実行中…",
+ "empty_response": "モデルから空の応答が返されました。もう一度送信してください。",
+ "empty_response_retry": "応答が空でした。リトライ中…"
},
"errors": {
"generic": "エラーが発生しました",
diff --git a/src/ollama_web/llm.py b/src/ollama_web/llm.py
index 48c177a..76a61a5 100644
--- a/src/ollama_web/llm.py
+++ b/src/ollama_web/llm.py
@@ -393,6 +393,9 @@ def stream_chat_with_tools(
cumulative_tool_chars = 0
+ MAX_EMPTY_RETRIES = 2
+ empty_retries = 0
+
rounds = 0
while rounds < MAX_TOOL_ROUNDS:
rounds += 1
@@ -406,6 +409,7 @@ def stream_chat_with_tools(
model,
cumulative_tool_chars,
)
+
t0 = time.time()
try:
stream = cast(Any, raw_client).chat(
@@ -449,6 +453,35 @@ def stream_chat_with_tools(
time.time() - t0,
)
+ # Detect empty responses. Some models / proxies occasionally return a
+ # stream with almost no meaningful content. Log it and retry a few
+ # times before giving up.
+ is_empty = (
+ not full_message.get("content")
+ and not full_message.get("thinking")
+ and not tool_calls
+ )
+ if is_empty and empty_retries < MAX_EMPTY_RETRIES:
+ empty_retries += 1
+ logger.warning(
+ "ollama returned an empty stream (chunks=%d); retry %d/%d",
+ chunk_count,
+ empty_retries,
+ MAX_EMPTY_RETRIES,
+ )
+ yield {
+ "type": "status",
+ "message": t("status.empty_response_retry", lang=language),
+ }
+ # Retry the same round without consuming the tool round budget.
+ rounds -= 1
+ # Small pause to avoid hammering the upstream.
+ time.sleep(0.5)
+ continue
+ # Reset retry budget on any non-empty round so isolated hiccups are fine.
+ if not is_empty:
+ empty_retries = 0
+
# Reconstruct the assistant message including any tool_calls so the
# next round can see them.
if tool_calls:
@@ -458,6 +491,12 @@ def stream_chat_with_tools(
messages.append(msg)
if not tool_calls:
+ if is_empty:
+ # Ran out of tool rounds while still empty; surface a warning.
+ yield {
+ "type": "warning",
+ "message": t("status.empty_response", lang=language),
+ }
done_event = {"type": "done"}
tokens = _token_counts(last_chunk)
if tokens:
diff --git a/src/ollama_web/sessions.py b/src/ollama_web/sessions.py
index 593f2d7..388066b 100644
--- a/src/ollama_web/sessions.py
+++ b/src/ollama_web/sessions.py
@@ -243,19 +243,23 @@ class SessionStore:
return session
def _update_session_token_count(self, session: dict[str, Any]) -> None:
- """Recalculate cumulative token_count for the session."""
- total_prompt = 0
- total_eval = 0
- for m in session.get("messages", []):
- tc = m.get("token_count") or {}
- total_prompt += int(tc.get("prompt_eval_count", 0) or 0)
- total_eval += int(tc.get("eval_count", 0) or 0)
- if total_prompt or total_eval:
- session["token_count"] = {
- "prompt_eval_count": total_prompt,
- "eval_count": total_eval,
- "total_count": total_prompt + total_eval,
- }
+ """Update the session token_count from the most recent assistant message.
+
+ ``prompt_eval_count`` is the input token count for the *entire* conversation
+ as seen by ollama in that response, not a per-message value. Summing it
+ across messages would over-count massively. We therefore keep the latest
+ assistant message's token_count as the authoritative session value.
+ """
+ messages = session.get("messages", [])
+ latest_assistant_tc = None
+ for m in reversed(messages):
+ if m.get("role") == "assistant":
+ tc = m.get("token_count")
+ if tc:
+ latest_assistant_tc = tc
+ break
+ if latest_assistant_tc is not None:
+ session["token_count"] = dict(latest_assistant_tc)
else:
session.pop("token_count", None)
diff --git a/src/ollama_web/static/app.js b/src/ollama_web/static/app.js
index 378e39f..54fb2e0 100644
--- a/src/ollama_web/static/app.js
+++ b/src/ollama_web/static/app.js
@@ -714,6 +714,17 @@ async function send() {
waiting.textContent = ev.message || t("chat.processing");
assistantBody.appendChild(waiting);
}
+ } else if (ev.type === "warning") {
+ gotError = true;
+ if (!placeholderCleared) {
+ assistantBody.innerHTML = "";
+ placeholderCleared = true;
+ }
+ const span = document.createElement("span");
+ span.className = "warning-msg";
+ span.style.color = "#f0883e";
+ span.textContent = ev.message || t("status.empty_response");
+ assistantBody.appendChild(span);
} else if (ev.type === "thinking") {
if (!thinkingBody) thinkingBody = addThinkingBubble(assistantExtras);
thinkingBody.textContent += ev.content;
diff --git a/tests/test_clear_messages.py b/tests/test_clear_messages.py
index c3f3595..a0653ca 100644
--- a/tests/test_clear_messages.py
+++ b/tests/test_clear_messages.py
@@ -82,6 +82,7 @@ def test_add_message_stores_token_count_and_clears_it():
assert reloaded["token_count"] == {
"prompt_eval_count": 120,
"eval_count": 80,
+ "num_ctx": 8192,
"total_count": 200,
}