Skip to content

Commit aa162b0

Browse files
ericdalloeca-agent
andcommitted
Fix Divide by zero in chat auto-compact for zero-limit models
models.dev returns 0 for context/output limits on image-only models (e.g. openai/chatgpt-image-latest). The previous guard in auto-compact? treated 0 as truthy, so the percentage calculation divided by zero and crashed the prompt flow. - Normalize non-positive limits to nil at the eca.models ingestion layer - Tighten auto-compact? to require a positive :context limit - Add regression tests for auto-compact? 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
1 parent ff4bedb commit aa162b0

4 files changed

Lines changed: 87 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Bugfix: avoid `Divide by zero` crash in chat auto-compact when models.dev reports `0` for a model's context/output limits (e.g. `openai/chatgpt-image-latest`); such limits are now normalized to `nil` and `auto-compact?` skips models without a known positive context window.
6+
57
## 0.130.1
68

79
- Add configurable skill paths and recursive directory loading for configured rules, commands, and skills; local skills are also discovered from `.agents/skills`. #423

src/eca/features/chat/lifecycle.clj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919
(not (get-in db [:chats chat-id :auto-compacting?])))
2020
(let [compact-threshold (or (get-in config [:agent agent-name :autoCompactPercentage])
2121
(get-in config [:autoCompactPercentage]))
22-
{:keys [session-tokens limit]} (shared/usage-sumary chat-id full-model db)]
23-
(when (and compact-threshold session-tokens (:context limit))
24-
(let [current-percentage (* (/ session-tokens (:context limit)) 100)]
22+
{:keys [session-tokens limit]} (shared/usage-sumary chat-id full-model db)
23+
context-limit (:context limit)]
24+
(when (and compact-threshold
25+
session-tokens
26+
(number? context-limit)
27+
(pos? context-limit))
28+
(let [current-percentage (* (/ session-tokens context-limit) 100)]
2529
(>= current-percentage compact-threshold))))))
2630

2731
(defn send-content! [{:keys [messenger chat-id parent-chat-id]} role content]

src/eca/models.clj

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@
6262

6363
(def ^:private one-million 1000000)
6464

65+
(defn ^:private pos-num
66+
"Return n when n is a positive number, otherwise nil. Used to drop
67+
meaningless 0/non-positive limits coming from upstream catalogs (e.g.
68+
models.dev returning 0 for image-only models like
69+
`openai/chatgpt-image-latest`)."
70+
[n]
71+
(when (and (number? n) (pos? n)) n))
72+
6573
(def ^:private models-with-web-search-support
6674
#{"openai/gpt-4.1"
6775
"openai/gpt-5.2"
@@ -101,9 +109,9 @@
101109
;; maybe fixed after web-search toolcall is implemented
102110
:web-search (contains? models-with-web-search-support (str provider "/" model))
103111
:tools (get model-config "tool_call")
104-
:max-output-tokens (get-in model-config ["limit" "output"])}
105-
:limit {:context (get-in model-config ["limit" "context"])
106-
:output (get-in model-config ["limit" "output"])}
112+
:max-output-tokens (pos-num (get-in model-config ["limit" "output"]))}
113+
:limit {:context (pos-num (get-in model-config ["limit" "context"]))
114+
:output (pos-num (get-in model-config ["limit" "output"]))}
107115
:input-token-cost (some-> (get-in model-config ["cost" "input"]) float (/ one-million))
108116
:output-token-cost (some-> (get-in model-config ["cost" "output"]) float (/ one-million))
109117
:input-cache-creation-token-cost (some-> (get-in model-config ["cost" "cache_write"]) float (/ one-million))
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
(ns eca.features.chat.lifecycle-test
2+
(:require
3+
[clojure.test :refer [deftest is testing]]
4+
[eca.features.chat.lifecycle :as lifecycle]))
5+
6+
(set! *warn-on-reflection* true)
7+
8+
(def ^:private chat-id "c1")
9+
(def ^:private agent-name :default)
10+
(def ^:private full-model "openai/test")
11+
(def ^:private config {:autoCompactPercentage 75})
12+
13+
(defn- db-with [usage model-caps]
14+
{:chats {chat-id {:usage usage}}
15+
:models {full-model model-caps}})
16+
17+
(deftest auto-compact?-does-not-crash-on-zero-or-missing-context-limit
18+
(testing "zero context limit (e.g. models.dev returning 0 for image-only models) is treated as unknown and returns falsy"
19+
(let [db (db-with {:last-input-tokens 100 :last-output-tokens 50}
20+
{:limit {:context 0 :output 0}})]
21+
(is (not (lifecycle/auto-compact? chat-id agent-name full-model config db)))))
22+
23+
(testing "nil context limit returns falsy"
24+
(let [db (db-with {:last-input-tokens 100 :last-output-tokens 50}
25+
{:limit {:context nil :output nil}})]
26+
(is (not (lifecycle/auto-compact? chat-id agent-name full-model config db)))))
27+
28+
(testing "missing :limit map returns falsy"
29+
(let [db (db-with {:last-input-tokens 100 :last-output-tokens 50}
30+
{})]
31+
(is (not (lifecycle/auto-compact? chat-id agent-name full-model config db)))))
32+
33+
(testing "unknown model (no entry in :models) returns falsy"
34+
(let [db {:chats {chat-id {:usage {:last-input-tokens 100 :last-output-tokens 50}}}}]
35+
(is (not (lifecycle/auto-compact? chat-id agent-name full-model config db))))))
36+
37+
(deftest auto-compact?-with-positive-context-limit
38+
(testing "returns truthy when usage meets/exceeds the configured threshold"
39+
(let [db (db-with {:last-input-tokens 800 :last-output-tokens 0}
40+
{:limit {:context 1000 :output 1000}})]
41+
(is (lifecycle/auto-compact? chat-id agent-name full-model config db))))
42+
43+
(testing "returns false when usage is below the configured threshold"
44+
(let [db (db-with {:last-input-tokens 100 :last-output-tokens 0}
45+
{:limit {:context 1000 :output 1000}})]
46+
(is (false? (lifecycle/auto-compact? chat-id agent-name full-model config db)))))
47+
48+
(testing "respects per-agent autoCompactPercentage override"
49+
(let [db (db-with {:last-input-tokens 500 :last-output-tokens 0}
50+
{:limit {:context 1000 :output 1000}})]
51+
(is (lifecycle/auto-compact? chat-id agent-name full-model
52+
{:agent {agent-name {:autoCompactPercentage 40}}
53+
:autoCompactPercentage 99}
54+
db)))))
55+
56+
(deftest auto-compact?-respects-in-progress-flags
57+
(testing "returns nil when chat is already compacting"
58+
(let [db (-> (db-with {:last-input-tokens 800 :last-output-tokens 0}
59+
{:limit {:context 1000 :output 1000}})
60+
(assoc-in [:chats chat-id :compacting?] true))]
61+
(is (nil? (lifecycle/auto-compact? chat-id agent-name full-model config db)))))
62+
63+
(testing "returns nil when chat is already auto-compacting"
64+
(let [db (-> (db-with {:last-input-tokens 800 :last-output-tokens 0}
65+
{:limit {:context 1000 :output 1000}})
66+
(assoc-in [:chats chat-id :auto-compacting?] true))]
67+
(is (nil? (lifecycle/auto-compact? chat-id agent-name full-model config db))))))

0 commit comments

Comments
 (0)