From df314cdc168fc97bb8cbd69104b4532cb78c2b89 Mon Sep 17 00:00:00 2001 From: Jakub Zika Date: Fri, 17 Jul 2026 18:32:58 +0200 Subject: [PATCH] Honor configured provider output limits --- CHANGELOG.md | 1 + src/eca/llm_api.clj | 1 + src/eca/llm_providers/ollama.clj | 15 ++++++++------ src/eca/llm_providers/openai_chat.clj | 4 ++-- test/eca/llm_api_test.clj | 20 +++++++++++++++++++ test/eca/llm_providers/ollama_test.clj | 22 +++++++++++++++++++++ test/eca/llm_providers/openai_chat_test.clj | 22 +++++++++++++++++++++ 7 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 307be6f84..42152ac24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Truncate oversized failed tool output, preserving full logs on disk instead of overflowing the model context. +- Honor model output limits in OpenAI-compatible Chat Completions and Ollama requests while preserving `extraPayload` overrides. ## 0.147.3 diff --git a/src/eca/llm_api.clj b/src/eca/llm_api.clj index e4be3908e..1aff739e0 100644 --- a/src/eca/llm_api.clj +++ b/src/eca/llm_api.clj @@ -397,6 +397,7 @@ :user-messages user-messages :past-messages past-messages :tools tools + :max-output-tokens max-output-tokens :extra-payload extra-payload :extra-headers extra-headers} callbacks) diff --git a/src/eca/llm_providers/ollama.clj b/src/eca/llm_providers/ollama.clj index fd6c69af4..68777d9cd 100644 --- a/src/eca/llm_providers/ollama.clj +++ b/src/eca/llm_providers/ollama.clj @@ -138,7 +138,8 @@ })))) messages)) -(defn chat! [{:keys [model user-messages reason? instructions api-url past-messages tools extra-headers extra-payload]} +(defn chat! [{:keys [model user-messages reason? instructions api-url past-messages tools max-output-tokens + extra-headers extra-payload]} {:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-reason] :as callbacks}] (let [messages (concat @@ -146,11 +147,13 @@ (normalize-messages user-messages)) stream? (boolean callbacks) body (deep-merge - {:model model - :messages messages - :think reason? - :tools (->tools tools) - :stream stream?} + (cond-> {:model model + :messages messages + :think reason? + :tools (->tools tools) + :stream stream?} + max-output-tokens + (assoc :options {:num_predict max-output-tokens})) extra-payload) url (join-api-url api-url chat-path) tool-calls* (atom {}) diff --git a/src/eca/llm_providers/openai_chat.clj b/src/eca/llm_providers/openai_chat.clj index c5192ed0c..f8bd929db 100644 --- a/src/eca/llm_providers/openai_chat.clj +++ b/src/eca/llm_providers/openai_chat.clj @@ -523,7 +523,7 @@ and message normalization. Supports both single and parallel tool execution. Compatible with OpenRouter and other OpenAI-compatible providers." [{:keys [model user-messages instructions temperature api-key api-url url-relative-path - past-messages tools extra-payload extra-headers supports-image? + max-output-tokens past-messages tools extra-payload extra-headers supports-image? think-tag-start think-tag-end reasoning-history http-client]} {:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-reason on-usage-updated] :as callbacks}] (let [think-tag-start (or think-tag-start "") @@ -541,7 +541,7 @@ {:model model :messages messages :stream stream? - :max_completion_tokens 32000} + :max_completion_tokens (or max-output-tokens 32000)} :temperature temperature :tools (when (seq tools) (->tools tools)) ;; Required by the OpenAI streaming spec to receive a final diff --git a/test/eca/llm_api_test.clj b/test/eca/llm_api_test.clj index 95a89468f..bc715622b 100644 --- a/test/eca/llm_api_test.clj +++ b/test/eca/llm_api_test.clj @@ -5,6 +5,7 @@ [eca.config :as config] [eca.llm-api :as llm-api] [eca.llm-providers.anthropic :as llm-providers.anthropic] + [eca.llm-providers.ollama :as llm-providers.ollama] [eca.llm-providers.openai :as llm-providers.openai] [eca.llm-providers.openai-chat :as llm-providers.openai-chat] [eca.secrets :as secrets] @@ -220,6 +221,25 @@ (is (= :openai-chat (:api (llm-api/provider->api-handler "github-copilot" "unknown-model" config))))))) +(deftest prompt-forwards-max-output-tokens-to-ollama-test + (let [captured* (atom nil)] + (with-redefs [llm-providers.ollama/chat! + (fn [opts _callbacks] + (reset! captured* opts) + {:output-text "ok"})] + (#'eca.llm-api/prompt! + {:provider "ollama" + :model "test-model" + :model-capabilities {:tools false + :reason? false + :model-name "test-model" + :max-output-tokens 512} + :user-messages [{:role "user" :content [{:type :text :text "hello"}]}] + :past-messages [] + :config {:providers {"ollama" {:url "http://localhost:11434"}}} + :sync? true})) + (is (= 512 (:max-output-tokens @captured*))))) + (deftest prompt-test (testing "Custom OpenAI provider behavior and proper passing of httpClient options to the Hato client" (let [req* (atom nil)] diff --git a/test/eca/llm_providers/ollama_test.clj b/test/eca/llm_providers/ollama_test.clj index 5bb6a541b..65289d231 100644 --- a/test/eca/llm_providers/ollama_test.clj +++ b/test/eca/llm_providers/ollama_test.clj @@ -80,6 +80,28 @@ (is (= {:output-text "Hello world"} result))))))) +(deftest chat-request-enforces-max-output-tokens-test + (let [requests* (atom [])] + (with-client-proxied {} + (fn handler [req] + (swap! requests* conj req) + {:status 200 + :body {:message {:content "ok"}}}) + (let [base-opts {:model "test-model" + :instructions "System prompt" + :user-messages [{:role "user" :content "hello"}] + :past-messages [] + :tools nil + :api-url "http://localhost:1" + :max-output-tokens 512}] + (llm-providers.ollama/chat! base-opts nil) + (llm-providers.ollama/chat! + (assoc base-opts :extra-payload {:options {:num_predict 99}}) nil))) + (let [bodies (mapv #(json/parse-string (:body %) true) @requests*)] + (is (= 512 (get-in bodies [0 :options :num_predict]))) + (is (= 99 (get-in bodies [1 :options :num_predict])) + "Configured extraPayload must remain the final override")))) + (deftest ->normalize-messages-test (testing "no previous history" (is (match? diff --git a/test/eca/llm_providers/openai_chat_test.clj b/test/eca/llm_providers/openai_chat_test.clj index 820f3cef8..9c7aea1bf 100644 --- a/test/eca/llm_providers/openai_chat_test.clj +++ b/test/eca/llm_providers/openai_chat_test.clj @@ -51,6 +51,28 @@ (is (= {:output-text "Hello there!"} (select-keys response [:output-text])))))))) +(deftest chat-request-enforces-max-output-tokens-test + (let [requests* (atom [])] + (with-client-proxied {} + (fn handler [req] + (swap! requests* conj req) + {:status 200 + :body {:choices [{:message {:content "ok"}}]}}) + (let [base-opts {:model "test-model" + :instructions "System prompt" + :user-messages [{:role "user" :content "hello"}] + :past-messages [] + :tools nil + :api-key "fake-key" + :api-url "http://localhost:1" + :max-output-tokens 512}] + (llm-providers.openai-chat/chat-completion! base-opts nil) + (llm-providers.openai-chat/chat-completion! + (assoc base-opts :extra-payload {:max_completion_tokens 99}) nil))) + (is (= 512 (get-in @requests* [0 :body :max_completion_tokens]))) + (is (= 99 (get-in @requests* [1 :body :max_completion_tokens])) + "Configured extraPayload must remain the final override"))) + (deftest normalize-messages-test (testing "With tool_call history - assistant text and tool calls are merged" (is (match?