<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Julia Denysova</title>
    <description>The latest articles on DEV Community by Julia Denysova (@julia_denysova).</description>
    <link>https://dev.to/julia_denysova</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022994%2Fd0e22106-80c9-4ecd-a97d-4f6b55635e5a.jpg</url>
      <title>DEV Community: Julia Denysova</title>
      <link>https://dev.to/julia_denysova</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julia_denysova"/>
    <language>en</language>
    <item>
      <title>Spring AI Retries and Embeddings: Failed Answers and Full Re-indexes — LLM Cost Control 4/4</title>
      <dc:creator>Julia Denysova</dc:creator>
      <pubDate>Tue, 25 Aug 2026 07:10:00 +0000</pubDate>
      <link>https://dev.to/julia_denysova/spring-ai-retries-and-embeddings-failed-answers-and-full-re-indexes-llm-cost-control-44-3i53</link>
      <guid>https://dev.to/julia_denysova/spring-ai-retries-and-embeddings-failed-answers-and-full-re-indexes-llm-cost-control-44-3i53</guid>
      <description>&lt;p&gt;Across the first three parts of this series, the goal has stayed the same: send fewer tokens without losing what the model actually needs to answer well. &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Part 1&lt;/a&gt; started with measurement and model choice, so you know what you are paying for before you optimise anything. &lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2&lt;/a&gt; looked at the tokens you already send: how long the answer runs, how much history you carry on every turn, and how caching can make repeated content cheaper. &lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9"&gt;Part 3&lt;/a&gt; tuned retrieval and tool calling, so a request now carries only the context that question actually needs.&lt;/p&gt;

&lt;p&gt;All of that assumes each request is sent only once. In practice, a failed answer usually means a retry, and a retry sends everything again — so one usable answer can cost you several requests.&lt;/p&gt;

&lt;p&gt;This part is about controls for failed responses and for the indexing side of RAG. It also closes the series: all ten drivers are listed on one map at the end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver #8 — Malformed output: paying full price for a failed answer&lt;/li&gt;
&lt;li&gt;Driver #9 — Embeddings at scale: small numbers, big multipliers&lt;/li&gt;
&lt;li&gt;The one-glance map: all ten drivers&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Prices: list prices where a ratio matters, an example rate of $1 per million input tokens elsewhere. &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Full note in Part 1&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Driver #8 — Malformed output: paying full price for a failed answer
&lt;/h2&gt;

&lt;p&gt;When the model returns JSON that your code cannot parse, the usual fix is to retry the call. Each retry resends the request context — system prompt, conversation history, RAG context, and tool schemas — so the input tokens are billed again and the failed answer is billed as output in full, cut off or not. For example, if 5% of requests require one retry, input token usage increases by roughly 5% without producing additional business value. A retry policy allowing three retries can make worst-case requests cost up to four times more.&lt;/p&gt;

&lt;p&gt;Spring AI's &lt;code&gt;.entity()&lt;/code&gt; convenience method on &lt;code&gt;ChatClient&lt;/code&gt;'s &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/ChatClient.CallResponseSpec.html" rel="noopener noreferrer"&gt;&lt;code&gt;CallResponseSpec&lt;/code&gt;&lt;/a&gt; traditionally used a prompt-based structured output approach: the &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/converter/StructuredOutputConverter.html" rel="noopener noreferrer"&gt;&lt;code&gt;StructuredOutputConverter&lt;/code&gt;&lt;/a&gt; generated format instructions (including the JSON schema) and added them to the prompt. The model was then expected to return JSON matching that schema, which Spring AI parsed into the target Java type afterwards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html" rel="noopener noreferrer"&gt;Spring AI 2.0&lt;/a&gt; adds two per-call controls through &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/ChatClient.EntityParamSpec.html" rel="noopener noreferrer"&gt;&lt;code&gt;EntityParamSpec&lt;/code&gt;&lt;/a&gt;: &lt;code&gt;.validateSchema()&lt;/code&gt; and &lt;code&gt;.useProviderStructuredOutput()&lt;/code&gt;. The first enables schema validation; the second switches from prompt-based instructions to the provider's native structured output mechanism, where supported.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="nc"&gt;Ticket&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Ticket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;useProviderStructuredOutput&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// schema enforced by the provider&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validateSchema&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;             &lt;span class="c1"&gt;// fallback: validate and retry&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.useProviderStructuredOutput()&lt;/code&gt; sends the schema to the provider as an API-level rule instead of as prompt text. The provider runtime enforces schema conformance for supported models, so invalid structured responses are prevented during generation. The schema instructions also disappear from the prompt, reducing input token usage on every call. Spring AI 2.0 &lt;a href="https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/structured-output/native.html#_supported_models" rel="noopener noreferrer"&gt;supports this for providers&lt;/a&gt; with native structured output capabilities, including OpenAI, Anthropic, Google GenAI, Mistral AI, and model-dependent Ollama support depending on the model. Check the &lt;a href="https://docs.spring.io/spring-ai/reference/api/structured-output-converter.html#_known_limitations" rel="noopener noreferrer"&gt;limitations&lt;/a&gt; for your provider before relying on it: OpenAI, for example, rejects top-level array schemas, so requesting a &lt;code&gt;List&amp;lt;T&amp;gt;&lt;/code&gt; fails, and Ollama models with reasoning mode may still return plain text.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.validateSchema()&lt;/code&gt; adds &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/advisor/StructuredOutputValidationAdvisor.html" rel="noopener noreferrer"&gt;&lt;code&gt;StructuredOutputValidationAdvisor&lt;/code&gt;&lt;/a&gt;: the response is checked against the schema, and if validation fails, the validation error is appended to the user message and the model is called again, &lt;a href="https://docs.spring.io/spring-ai/reference/2.0-SNAPSHOT/api/structured-output/validation.html#_customizing_the_advisor" rel="noopener noreferrer"&gt;up to 3 retry attempts by default&lt;/a&gt;. Each retry is another model call and consumes tokens, so use validation as a recovery mechanism rather than the primary strategy. Combine it with provider-native structured output when available to prevent failures before they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #9 — Embeddings at scale: small numbers, big multipliers
&lt;/h2&gt;

&lt;p&gt;Embedding calls look cheap on a per-request basis, but a RAG pipeline runs them across your entire document collection, and may run them again whenever documents are added, updated, or a full re-index is required.&lt;/p&gt;

&lt;p&gt;Three things drive that cost: the embedding model's price, the dimensionality of the vectors (which affects storage and retrieval costs), and how much text you actually need to embed during each indexing run. A fourth source of embedding calls has nothing to do with documents at all — that one comes at the end.&lt;/p&gt;

&lt;h3&gt;
  
  
  The embedding model
&lt;/h3&gt;

&lt;p&gt;Model price comes first. According to OpenAI's pricing (August 2026), &lt;a href="https://developers.openai.com/api/docs/models/text-embedding-3-small" rel="noopener noreferrer"&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt;&lt;/a&gt; costs $0.02 per million input tokens, compared to $0.13 per million for &lt;a href="https://developers.openai.com/api/docs/models/text-embedding-3-large" rel="noopener noreferrer"&gt;&lt;code&gt;text-embedding-3-large&lt;/code&gt;&lt;/a&gt; — making the small model about 6.5× cheaper. Embedding a 50-million-token document collection therefore costs about $1 with the small model versus $6.50 with the large one each time you re-embed the entire corpus, such as during a full re-index. For many retrieval workloads, the smaller model delivers sufficient retrieval quality. Rather than assuming the larger model is necessary, evaluate both using the same fixed test set of questions introduced in &lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9#driver-6-rag-context-stuffing-paying-for-noise"&gt;Driver #6, Part 3&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector dimensionality
&lt;/h3&gt;

&lt;p&gt;The second lever is vector dimensionality. &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/embedding/EmbeddingOptions.html" rel="noopener noreferrer"&gt;&lt;code&gt;EmbeddingOptions&lt;/code&gt;&lt;/a&gt; exposes a &lt;code&gt;dimensions&lt;/code&gt; setting for providers that support it, such as &lt;a href="https://docs.spring.io/spring-ai/reference/api/embeddings/openai-embeddings.html" rel="noopener noreferrer"&gt;OpenAI's text-embedding-3 models&lt;/a&gt; (including deployments on Azure). Reducing the number of dimensions does not lower the embedding API cost, which is based on input tokens, but it significantly reduces vector storage requirements. Assuming float32 vectors (4 bytes per dimension), one million vectors at 3,072 dimensions occupy about 12 GB of raw storage, while 512-dimensional vectors require about 2 GB. Smaller vectors also reduce index size and memory usage and often reduce similarity search latency because less data must be stored and processed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;
&lt;span class="py"&gt;spring.ai.openai.embedding.model&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;text-embedding-3-small&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.openai.embedding.dimensions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;512&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the number of dimensions before you start loading data. A vector index normally requires all vectors to have the same dimensionality, so changing this setting later usually requires creating a new index and re-embedding the existing documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batching and incremental indexing
&lt;/h3&gt;

&lt;p&gt;Third: batch requests, and skip what has not changed. When you add documents through a &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/VectorStore.html" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStore&lt;/code&gt;&lt;/a&gt;, Spring AI uses the &lt;a href="https://docs.spring.io/spring-ai/reference/api/vectordbs.html#_default_implementation" rel="noopener noreferrer"&gt;default&lt;/a&gt; &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/embedding/TokenCountBatchingStrategy.html" rel="noopener noreferrer"&gt;&lt;code&gt;TokenCountBatchingStrategy&lt;/code&gt;&lt;/a&gt;, which groups documents into batches based on token count. It keeps batches below the embedding model's token limit (OpenAI's 8,191-token limit with a default 10% safety margin) instead of sending each chunk as a separate embedding request. You can replace this with your own &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/embedding/BatchingStrategy.html" rel="noopener noreferrer"&gt;&lt;code&gt;BatchingStrategy&lt;/code&gt;&lt;/a&gt; bean if needed.&lt;/p&gt;

&lt;p&gt;What the framework cannot know automatically is which documents actually changed. Store a content hash in each Document's metadata and re-embed only chunks whose hash differs. If only 2% of chunks require updating each week, embedding work can be roughly 50× smaller than a full re-index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chat memory is an embedding cost too
&lt;/h3&gt;

&lt;p&gt;Everything above assumes you are embedding documents. There is a second source, and it is easy to miss because it does not look like indexing at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i#driver-4-conversation-history-you-pay-for-the-whole-chat-every-turn"&gt;Part 2, Driver #4&lt;/a&gt; offered &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_vectorstorechatmemoryadvisor" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStoreChatMemoryAdvisor&lt;/code&gt;&lt;/a&gt; as an option for very long conversations. Instead of resending the whole chat on every turn, it saves the messages in a vector store and adds back only the ones that match the current question. Each request then stays the same size, however long the session runs. That is a real saving on the chat side.&lt;/p&gt;

&lt;p&gt;The cost does not disappear, though. It moves. Every message the advisor saves has to be turned into a vector first, and the search itself embeds the question too — so each turn brings its own embedding cost. A document collection is embedded once and updated from time to time; a busy support chat produces new messages all day, so here the number of embedding calls follows your traffic, not the size of your collection. The controls are the same — a smaller model, fewer dimensions — but count the calls before you choose this advisor over a message window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up: the one-glance map
&lt;/h2&gt;

&lt;p&gt;Every driver above follows the same basic pattern: tokens repeat or grow somewhere, and Spring AI gives you a control to cut them. Here is the full map:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Cost driver&lt;/th&gt;
&lt;th&gt;Symptom on the bill&lt;/th&gt;
&lt;th&gt;Spring AI control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo#driver-0-spring-ai-observability-measurement-you-cannot-cut-what-you-cannot-see"&gt;No measurement&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Unexpected costs at the end of the month&lt;/td&gt;
&lt;td&gt;Micrometer token metrics; operational alerts; provider spending limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo#driver-1-model-choice-stop-paying-flagship-prices-for-minimodel-work"&gt;Model choice&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Flagship prices for routine tasks&lt;/td&gt;
&lt;td&gt;configurable model selection; one &lt;code&gt;ChatClient&lt;/code&gt; per cost tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo#driver-2-one-shared-client-every-request-carries-every-default"&gt;One shared client&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Requests include defaults they do not need&lt;/td&gt;
&lt;td&gt;Per-task clients from the prototype &lt;code&gt;ChatClient.Builder&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i#driver-3-output-and-reasoning-tokens-the-expensive-direction"&gt;Output &amp;amp; reasoning tokens&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Unnecessarily long responses or expensive reasoning&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;maxTokens&lt;/code&gt; ceiling; thinking/reasoning-effort options (where supported)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i#driver-4-conversation-history-you-pay-for-the-whole-chat-every-turn"&gt;Conversation history&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Cost per request grows a lot as conversations get longer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MessageWindowChatMemory&lt;/code&gt; with a fixed window size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i#driver-5-repeated-static-content-paying-full-price-for-the-same-tokens"&gt;Repeated static content&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;The same prompt content is sent again and again&lt;/td&gt;
&lt;td&gt;Static-first prompt structure; provider prompt caching (where supported); &lt;code&gt;promptCacheKey&lt;/code&gt; on OpenAI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9#driver-6-rag-context-stuffing-paying-for-noise"&gt;RAG stuffing&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Too much retrieved content increases input tokens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;topK&lt;/code&gt;, similarity threshold, rerank/compress post-processors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9#driver-7-tool-schemas-and-agent-loops-paying-for-a-toolbox-you-rarely-open"&gt;Tool schemas &amp;amp; loops&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Tool definitions are sent unnecessarily; loops run too long&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ToolSearchToolCallingAdvisor&lt;/code&gt;; a custom ToolCallingAdvisor subclass for loop limits; McpToolFilter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Malformed output&lt;/td&gt;
&lt;td&gt;Failed responses require retries&lt;/td&gt;
&lt;td&gt;Native structured output (&lt;code&gt;ChatClient.EntityParamSpec.useProviderStructuredOutput()&lt;/code&gt;) with schema enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Embeddings at scale&lt;/td&gt;
&lt;td&gt;Indexing and vector storage costs keep growing&lt;/td&gt;
&lt;td&gt;Smaller embedding model; dimensions; batching; application-side incremental indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Ten drivers is a list, not a plan. Working through them in order is the slowest way to use this map, because the drivers are not equal — in most applications two or three of them carry almost the whole bill, and which ones depends on what you built.&lt;/p&gt;

&lt;p&gt;So start at Driver #0. Set up your metrics, watch prompt and completion tokens, and let the graph choose your next move.&lt;/p&gt;

&lt;p&gt;One last thing worth saying: every control in this series trades something away. A smaller window forgets more. A tighter &lt;code&gt;topK&lt;/code&gt; misses more facts. A lower reasoning effort reasons less. None of these is free, and a cheap application that gives poor answers is not cheap — users simply ask again. The main point here is knowing what each token buys you, and paying only for the ones that buy something.&lt;/p&gt;

&lt;p&gt;That is the series. If you have hit a cost driver that is not on the map, or your numbers came out differently from the estimates here, the comments are the right place for it — several ideas from readers of &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Part 1&lt;/a&gt; ended up shaping the later parts.&lt;/p&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>springboot</category>
      <category>llm</category>
    </item>
    <item>
      <title>Spring AI RAG and Tool Calling: Paying for Context You Don't Use — LLM Cost Control 3/4</title>
      <dc:creator>Julia Denysova</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9</link>
      <guid>https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9</guid>
      <description>&lt;p&gt;Suppose responses now have a token limit, the memory window is set, and the prompt begins with content a provider can cache — that was &lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2&lt;/a&gt;. Every control there worked on content that was already in the request: how long the answer may be, how much history travels with it, how the fixed part is arranged. You set a limit or an order, and that was it.&lt;/p&gt;

&lt;p&gt;The next thing to look at is the context your application adds by itself at runtime. Document chunks arrive from a vector store, tool definitions arrive from every server you connect to. Both are billed as input tokens, both are sent whether the model uses them or not, and both grow with how much you have indexed or connected — not with what the current question actually needs.&lt;/p&gt;

&lt;p&gt;This part is about controls for retrieved context and tool schemas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver #6 — RAG context stuffing: paying for noise&lt;/li&gt;
&lt;li&gt;Driver #7 — Tool schemas and agent loops: paying for a toolbox you rarely open&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Prices: list prices where a ratio matters, an example rate of $1 per million input tokens elsewhere. &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Full note in Part 1&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Driver #6 — RAG context stuffing: paying for noise
&lt;/h2&gt;

&lt;p&gt;Every retrieved document chunk becomes input tokens on every request, whether the model actually needs it or not. RAG bills you for relevance you never checked: retrieve 10 chunks of about 800 tokens each, and every question now carries 8,000 tokens of context. At 50,000 requests a month, that is 400 million tokens — $400 a month for retrieval alone at the example rate. And if only 3 of those 10 chunks are actually relevant, roughly $280 of that is spent on noise. Noise costs you twice: you pay for the tokens, and irrelevant context also lowers answer quality, which tends to lead to retries and follow-up questions.&lt;/p&gt;

&lt;p&gt;The first control is retrieving less. Spring AI's &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/rag/retrieval/search/VectorStoreDocumentRetriever.html" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStoreDocumentRetriever&lt;/code&gt;&lt;/a&gt; offers two settings: &lt;code&gt;topK&lt;/code&gt; sets a hard limit on how many chunks are retrieved, and &lt;code&gt;similarityThreshold&lt;/code&gt; (0.0–1.0) filters out results whose similarity score falls below the specified threshold. By default, the retriever requests the top 4 results with a similarity threshold of 0.0, which disables similarity filtering and accepts all returned matches. As a result, the retriever accepts the vector store's nearest-neighbor results up to &lt;code&gt;topK&lt;/code&gt;, which can include weakly related chunks when no truly relevant documents exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="nc"&gt;Advisor&lt;/span&gt; &lt;span class="n"&gt;ragAdvisor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RetrievalAugmentationAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;documentRetriever&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VectorStoreDocumentRetriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;topK&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// retrieve at most 3 documents&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;similarityThreshold&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// exclude documents below the similarity threshold&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;advisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ragAdvisor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second control is cleaning up what was retrieved. &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/rag/advisor/RetrievalAugmentationAdvisor.html" rel="noopener noreferrer"&gt;&lt;code&gt;RetrievalAugmentationAdvisor&lt;/code&gt;&lt;/a&gt; is Spring AI's modular RAG pipeline, and its &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/rag/postretrieval/document/DocumentPostProcessor.html" rel="noopener noreferrer"&gt;&lt;code&gt;DocumentPostProcessor&lt;/code&gt;&lt;/a&gt; stage runs between document retrieval and prompt generation. This extension point lets you transform or filter the retrieved documents before they are added to the prompt. For example, you can implement custom post-processors to re-rank documents and keep only the most relevant ones, remove near-duplicate documents (common with overlapping chunking strategies), or compress each document to only the passages needed to answer the question. Each of these steps reduces the amount of context that reaches the model, lowering token usage while often improving answer quality.&lt;/p&gt;

&lt;p&gt;One thing to watch for: some RAG pipeline steps spend tokens in order to save tokens later. LLM-based compression or query rewriting adds additional model calls and token usage for each request. That trade can still be worth it — for example, using a cheaper model to compress context before sending it to a more expensive flagship model — but the optimization is not free, so measure both sides (&lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo#driver-0-spring-ai-observability-measurement-you-cannot-cut-what-you-cannot-see"&gt;Driver #0&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;A simple rule for tuning: if answers are missing relevant facts, try increasing &lt;code&gt;topK&lt;/code&gt; or lowering the similarity threshold; if prompts are too large or expensive, try reducing &lt;code&gt;topK&lt;/code&gt; or increasing the threshold. Evaluate changes against a fixed test set of questions rather than adjusting by intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #7 — Tool schemas and agent loops: paying for a toolbox you rarely open
&lt;/h2&gt;

&lt;p&gt;Every tool made available to the model is included in the chat request as a tool definition containing its name, description, and JSON schema. &lt;a href="https://docs.spring.io/spring-ai/reference/api/tools.html#tool-search-tool" rel="noopener noreferrer"&gt;Spring AI&lt;/a&gt; warns that large agent setups connected to services such as Slack, GitHub, Jira, and MCP servers can easily expose 50+ tools, consuming 55,000+ tokens before the first user message. It also notes that tool selection accuracy degrades when models face dozens of similarly named tools. At an example input price of $1 per million tokens, 55,000 schema tokens would cost about $0.055 per request, or $5,500 per 100,000 requests, before considering caching or tool-discovery approaches.&lt;/p&gt;

&lt;p&gt;In Spring AI 2.0, &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/advisor/ToolCallingAdvisor.html" rel="noopener noreferrer"&gt;&lt;code&gt;ToolCallingAdvisor&lt;/code&gt;&lt;/a&gt; is the standard tool execution mechanism used by &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/ChatClient.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatClient&lt;/code&gt;&lt;/a&gt;; the tool-call loop was moved out of individual &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/model/ChatModel.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatModel&lt;/code&gt;&lt;/a&gt; implementations and into the advisor chain. (&lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/advisor/ToolCallAdvisor.html" rel="noopener noreferrer"&gt;&lt;code&gt;ToolCallAdvisor&lt;/code&gt;&lt;/a&gt; was renamed during the 2.0 development cycle and remains available as a deprecated compatibility class.) By default, &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; still sends all available tool definitions to the model. For large tool libraries, &lt;a href="https://docs.spring.io/spring-ai/reference/api/tools.html#tool-search-tool" rel="noopener noreferrer"&gt;Spring AI 2.0&lt;/a&gt; provides &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/advisor/toolsearch/ToolSearchToolCallingAdvisor.html" rel="noopener noreferrer"&gt;&lt;code&gt;ToolSearchToolCallingAdvisor&lt;/code&gt;&lt;/a&gt;, which uses dynamic tool discovery: the model initially receives a search tool, then relevant tool definitions are added only when discovered. &lt;a href="https://docs.spring.io/spring-ai/reference/guides/dynamic-tool-search.html" rel="noopener noreferrer"&gt;Spring's benchmark reports&lt;/a&gt; 34–64% token reduction across OpenAI, Anthropic, and Gemini models, although actual savings depend on the number of tools, schema size, model pricing, and workload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;
&lt;span class="py"&gt;spring.ai.chat.client.tool-search-advisor.enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;spring.ai.chat.client.tool-search-advisor.tool-index-type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;lucene&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces the default &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; behind the scenes. Three &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/tool/toolsearch/ToolIndex.html" rel="noopener noreferrer"&gt;&lt;code&gt;ToolIndex&lt;/code&gt;&lt;/a&gt; implementations are &lt;a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_spring_boot_auto_configuration" rel="noopener noreferrer"&gt;available&lt;/a&gt;: &lt;code&gt;regex&lt;/code&gt; (the default — lightweight, no extra dependencies), &lt;code&gt;lucene&lt;/code&gt; keyword search (included with the starter), and &lt;code&gt;vector&lt;/code&gt; semantic search (needs a &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/VectorStore.html" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStore&lt;/code&gt;&lt;/a&gt; bean).&lt;/p&gt;

&lt;p&gt;The loop itself is another place where tokens can accumulate. &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; continues calling the model while responses contain tool calls, and every iteration is another model request carrying the conversation state accumulated so far. A model repeatedly failing the same tool call can therefore consume additional tokens. If you need an iteration limit as a hard safety boundary, as of today the only way is to build it yourself on the advisor's extension points — &lt;code&gt;ToolCallingAdvisor&lt;/code&gt; is designed for subclassing, with protected hook methods (such as &lt;code&gt;doBeforeCall&lt;/code&gt;) called on every iteration, and the accumulated history in each request already tells you how many tool rounds have run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BoundedToolCallingAdvisor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ToolCallingAdvisor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxIterations&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// constructor via the self-referential Builder pattern omitted for brevity&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="nc"&gt;ChatClientRequest&lt;/span&gt; &lt;span class="nf"&gt;doBeforeCall&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClientRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                                             &lt;span class="nc"&gt;CallAdvisorChain&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// method logic goes here    &lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A custom &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/model/tool/ToolExecutionEligibilityChecker.html" rel="noopener noreferrer"&gt;&lt;code&gt;ToolExecutionEligibilityChecker&lt;/code&gt;&lt;/a&gt; can control whether a returned tool call should be executed, but it only sees the latest response, so it cannot count rounds — it is not a replacement for a loop-iteration limit.&lt;/p&gt;

&lt;p&gt;If the default tool-calling loop needs additional guardrails, &lt;a href="https://docs.spring.io/spring-ai/reference/api/chatclient.html#_disabling_auto_registration" rel="noopener noreferrer"&gt;there are several levels of control&lt;/a&gt;. You can disable automatic registration globally through configuration or for a single ChatClient call when you want to manage tool execution yourself. For reusable policies such as custom stopping rules, tool-specific restrictions, or additional observability, implement a custom ToolCallingAdvisor. If the application requires a completely different agent workflow, take control of the loop directly at the ChatModel level and manage tool execution explicitly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; can make the schema problem larger: each connected server may contribute its available tool definitions, and a single session can end up exposing dozens  of tools across multiple servers. Two controls help here: an &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/mcp/McpToolFilter.html" rel="noopener noreferrer"&gt;&lt;code&gt;McpToolFilter&lt;/code&gt;&lt;/a&gt; bean (covered in the &lt;a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html#_tool_filtering" rel="noopener noreferrer"&gt;Tool Calling reference&lt;/a&gt;) lets you control which MCP tools are exposed, for example by server or tool metadata, while the tool-search advisor can index MCP-provided tool callbacks and retrieve only relevant tool definitions when they are needed. Setting &lt;code&gt;spring.ai.mcp.client.toolcallback.enabled=false&lt;/code&gt; &lt;a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html#_disable_the_mcp_toolcallback_auto_configuration" rel="noopener noreferrer"&gt;opts out&lt;/a&gt; of exposing MCP tools entirely. &lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Drivers #6 and #7 solve the same problem, just in different parts of the prompt — they keep the model from receiving context this question does not need. The first does it by retrieving and keeping fewer document chunks, the second by sending tool definitions only when the model asks for them.&lt;/p&gt;

&lt;p&gt;That holds as long as the request succeeds. When the model returns JSON your code cannot parse, the retry sends the whole request again — system prompt, history, retrieved chunks, tool schemas — and you pay for the same context a second time.&lt;/p&gt;

&lt;p&gt;Driver #6 also left something out. Before a vector store can return anything, every document in your collection has to be turned into a vector, and that is a paid API call for each chunk. The retrieval side of RAG is what this part explained; the indexing side has its own bill, and it arrives again whenever you re-index.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-retries-and-embeddings-failed-answers-and-full-re-indexes-llm-cost-control-44-3i53"&gt;Part 4&lt;/a&gt; covers those two, and closes the series with all ten drivers on a single map.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Spring AI Prompt Caching and Chat Memory: Where the Tokens Go — LLM Cost Control 2/4</title>
      <dc:creator>Julia Denysova</dc:creator>
      <pubDate>Tue, 04 Aug 2026 07:04:09 +0000</pubDate>
      <link>https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i</link>
      <guid>https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i</guid>
      <description>&lt;p&gt;Suppose the metrics are in place, each task has the model it actually needs, and every feature has its own client — that was &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Part 1&lt;/a&gt;. The next thing to look at is what those clients send and receive.&lt;/p&gt;

&lt;p&gt;This part is about controls for output token generation, chat memory and static input tokens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Driver #3 — Output and reasoning tokens: the expensive direction&lt;/li&gt;
&lt;li&gt;Driver #4 — Conversation history: you pay for the whole chat, every turn&lt;/li&gt;
&lt;li&gt;Driver #5 — Repeated static content: paying full price for the same tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Prices: list prices where a ratio matters, an example rate of $1 per million&lt;br&gt;
input tokens elsewhere. &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Full note in Part 1&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Driver #3 — Output and reasoning tokens: the expensive direction
&lt;/h2&gt;

&lt;p&gt;Every token the model generates costs several times more than a token you send it. Reasoning models also generate hidden "thinking" tokens, and these are billed at the same, higher output rate.&lt;/p&gt;

&lt;p&gt;The gap is on every price sheet. On &lt;a href="https://developers.openai.com/api/docs/pricing" rel="noopener noreferrer"&gt;OpenAI's list&lt;/a&gt; (August 2026), &lt;code&gt;gpt-5&lt;/code&gt; input costs $1.25 per million tokens and output costs $10 — an 8× difference. On &lt;a href="https://claude.com/pricing#api" rel="noopener noreferrer"&gt;Anthropic's price sheet&lt;/a&gt;, &lt;code&gt;Sonnet 5&lt;/code&gt; is $2 in and $10 out — a 5× difference. (That is introductory pricing to 31 August 2026; the standard $3/$15 keeps the same 5× ratio.)&lt;/p&gt;

&lt;p&gt;The exact numbers change often, but the pattern does not: output tokens have cost several times more than input tokens for years. Reasoning makes this worse. A model may use 2,000 thinking tokens to produce a 200-token answer, so you pay for 2,200 output tokens in total — eleven times the text the user actually sees. A verbose model on a busy endpoint can end up costing more than all your input traffic combined.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.spring.io/spring-ai/reference/index.html" rel="noopener noreferrer"&gt;Spring AI&lt;/a&gt; gives you two controls here. The first works everywhere: &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/prompt/ChatOptions.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatOptions.builder().maxTokens()&lt;/code&gt;&lt;/a&gt; provides a portable completion-length limit, when supported by the provider. Think of it as a safety net against runaway responses, not a tool for improving quality — a cut-off answer is still billed in full, so pair the limit with prompt instructions that ask for a short answer. The second control is provider-specific and kept isolated in that provider's options object: OpenAI has a &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html#_configuration_properties" rel="noopener noreferrer"&gt;reasoning-effort setting&lt;/a&gt;, Anthropic has a &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/anthropic-chat.html#_thinking_configuration" rel="noopener noreferrer"&gt;thinking-token budget&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="c1"&gt;// Provider-independent token limit &lt;/span&gt;
&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;capped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Provider-specific reasoning control, isolated in one options object&lt;/span&gt;
&lt;span class="nc"&gt;OpenAiChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;lowEffort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5-mini"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reasoningEffort&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"low"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxCompletionTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Ollama: disable reasoning for thinking-capable models&lt;/span&gt;
&lt;span class="nc"&gt;OllamaOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;noThink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"qwen3"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;think&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One 2.0 upgrade note belongs here: &lt;a href="https://docs.spring.io/spring-ai/reference/upgrade-notes.html#_anthropic_module" rel="noopener noreferrer"&gt;the Anthropic module's &lt;code&gt;maxTokens&lt;/code&gt; default rose from 500 to 4096&lt;/a&gt;. If you relied — even without noticing it — on the old 500-token cap to limit the cost of responses, they can now run up to eight times longer after the upgrade. Set the limit explicitly if you want to keep the old behaviour.&lt;/p&gt;

&lt;p&gt;One trap deserves its own warning. Several popular &lt;a href="https://ollama.com/search" rel="noopener noreferrer"&gt;Ollama models&lt;/a&gt; — including &lt;code&gt;qwen3&lt;/code&gt; and &lt;code&gt;deepseek-r1&lt;/code&gt; — use reasoning by default. During local development there is no per-token API bill, so it is easy to miss that a prompt pattern has become dependent on lengthy reasoning. Move the same prompts to a provider that bills reasoning tokens, and that hidden reasoning can become part of your completion cost. For simple workloads such as extraction, classification, or reformatting, disable or limit thinking during local development as well. This keeps token usage, latency, and production costs easier to predict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #4 — Conversation history: you pay for the whole chat, every turn
&lt;/h2&gt;

&lt;p&gt;LLMs have no built-in memory, so "memory" in practice means sending the full conversation history with every request. Every past message is billed again, as if it were new input.&lt;/p&gt;

&lt;p&gt;The cost grows faster than you might expect. Assume a 500-token system prompt, roughly 50-token user messages, and roughly 200-token replies. Each finished turn adds about 250 tokens of history, and every later turn has to carry that history too. A message window bounds how much of that history each request includes. Two details must be noted here: the window counts the stored messages, so ten messages means the last five complete user–assistant exchanges, and the current user message always travels outside the window. From turn 6 onwards, every request is therefore the same size: 500 + 1,250 + 50 = 1,800 input tokens.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Turn&lt;/th&gt;
&lt;th&gt;Input tokens, unbounded history&lt;/th&gt;
&lt;th&gt;Input tokens, window = 10 messages&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;550&lt;/td&gt;
&lt;td&gt;550&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1,550&lt;/td&gt;
&lt;td&gt;1,550&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;5,300&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;12,800&lt;/td&gt;
&lt;td&gt;1,800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Whole 50-turn session&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 333,750&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 86,250&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without a limit, the cost of each turn grows in a straight line, but the cost of the whole session grows much faster than that: a 20-turn session sends about 58,500 input tokens in total, and a 50-turn support chat about 333,750 — for a single user. The same 50-turn chat with a 10-message window sends about 86,250, roughly a quarter of the unbounded total.&lt;/p&gt;

&lt;p&gt;One Spring AI 2.0 detail is worth knowing here: &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_turn_boundary_eviction" rel="noopener noreferrer"&gt;eviction now removes whole turns&lt;/a&gt;. A turn starts at a user message, so the kept window always begins with one, and maxMessages is an upper bound rather than a guarantee. For plain chat, choose an even window size so it maps cleanly onto complete exchanges.&lt;/p&gt;

&lt;p&gt;Spring AI's control here is &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/memory/MessageWindowChatMemory.html" rel="noopener noreferrer"&gt;&lt;code&gt;MessageWindowChatMemory&lt;/code&gt;&lt;/a&gt;, a sliding window of at most N messages, added through a memory advisor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                      &lt;span class="nc"&gt;ChatMemoryRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;ChatMemory&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MessageWindowChatMemory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatMemoryRepository&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxMessages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// overrides the default 20-message window&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service that injects this client selects the conversation on each call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// mandatory in Spring AI 2.0 — there is no default conversation id&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;advisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMemory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONVERSATION_ID&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/memory/ChatMemory.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatMemory&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/memory/ChatMemoryRepository.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatMemoryRepository&lt;/code&gt;&lt;/a&gt; are documented together in Spring AI's &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html" rel="noopener noreferrer"&gt;Chat Memory reference&lt;/a&gt;. Spring AI sets this up automatically (an in-memory repository and a 20-message window). Treat &lt;code&gt;maxMessages&lt;/code&gt; as a cost-control decision, not just a convenience setting: a 20-message window from short Q&amp;amp;A may be cheap, while the same window from long conversations can add thousands of tokens to every request. &lt;code&gt;MessageWindowChatMemory&lt;/code&gt; limits the number of messages, not tokens, so choose the window size based on the typical token size of your conversations and the context you actually need.&lt;/p&gt;

&lt;p&gt;Two things to keep in mind. First, a smaller window trades cost for what the model remembers. The model genuinely forgets removed turns, so choose the smallest window your use case can accept, not simply the smallest window possible. Second, the window affects Driver #5 too: a stable start to the prompt (system prompt, then the oldest history) is what makes provider-side caching work well. Removing messages aggressively, in a way that changes the start of the prompt on every turn, can cost you the cache discount.&lt;/p&gt;

&lt;p&gt;For very long sessions, &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_vectorstorechatmemoryadvisor" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStoreChatMemoryAdvisor&lt;/code&gt;&lt;/a&gt; is an alternative. It stores history in a vector store and adds back only the messages relevant to the current question, so the input size per turn stays flat no matter how long the session runs. The trade-off: every message has to be turned into an embedding and stored (&lt;a href="https://dev.to/julia_denysova/spring-ai-retries-and-embeddings-failed-answers-and-full-re-indexes-llm-cost-control-44-3i53"&gt;Part 4&lt;/a&gt;), and the history added back changes with each question, which works against caching. Caching is next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #5 — Repeated static content: paying full price for the same tokens
&lt;/h2&gt;

&lt;p&gt;The system prompt, tool definitions (including schemas), and few-shot examples are often the same across requests, and they are part of the model input context. Without caching, providers generally process and charge these repeated input tokens on every request. Prompt caching reduces this cost: supported providers store already-processed prompt prefixes and apply a lower input-token rate when the same content is reused. Every provider implements caching differently, with its own rules for eligibility, expiration, and configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anthropic and AWS Bedrock: you choose what to cache
&lt;/h3&gt;

&lt;p&gt;Spring AI exposes named caching strategies — &lt;code&gt;SYSTEM_ONLY&lt;/code&gt;, &lt;code&gt;TOOLS_ONLY&lt;/code&gt;, &lt;code&gt;SYSTEM_AND_TOOLS&lt;/code&gt;, and &lt;code&gt;CONVERSATION_HISTORY&lt;/code&gt; — through provider-specific enums for &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/anthropic-chat.html#_caching_strategies" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt; and &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/bedrock-converse.html#_cache_strategies" rel="noopener noreferrer"&gt;AWS Bedrock&lt;/a&gt;. These strategies define where Spring AI places cache breakpoints while respecting provider limitations, but cache lifetime and expiration remain managed by the underlying provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="nc"&gt;AnthropicChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cacheOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnthropicCacheOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AnthropicCacheStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SYSTEM_AND_TOOLS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anthropic prompt caching &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;charges&lt;/a&gt; a higher price for cache writes and a lower price for cache reads, which makes the feature valuable for workloads with repeated, stable prompts. For example, at $1 per million input tokens, a 2,000-token system prompt sent 300,000 times per month costs $600 without caching; with a high cache hit rate and 0.1× cached reads, the cost can drop to roughly $60. The main things to watch are cache lifetime and traffic patterns: caches expire, and low-traffic endpoints may pay cache-write costs without enough cache hits to recover the overhead.&lt;/p&gt;

&lt;p&gt;For agent-style workloads, there is one more Anthropic-specific setting worth knowing: &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/anthropic-chat.html#_caching_tool_results" rel="noopener noreferrer"&gt;&lt;code&gt;cacheToolResults&lt;/code&gt;&lt;/a&gt;. With &lt;code&gt;CONVERSATION_HISTORY&lt;/code&gt; caching, each tool-calling round adds tool results after the default cache point, so those tool outputs are billed as new, uncached input on later rounds. Enabling &lt;code&gt;cacheToolResults&lt;/code&gt; moves the cache point to the last tool result, allowing the next round to read the previous round's (often large) tool output from the cache instead of processing it again. This pairs directly with tool schemas and agent loops (&lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9"&gt;Part 3&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenAI: automatic, but no longer free
&lt;/h3&gt;

&lt;p&gt;Prompts of at least 1,024 tokens are cached automatically, with no code changes. Two things changed with the GPT-5.6 family, and &lt;a href="//developers.openai.com/api/docs/guides/prompt-caching"&gt;both affect the bill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, cache writes are billed at 1.25× the uncached input rate. On earlier models they were free.&lt;/p&gt;

&lt;p&gt;Second, the service caches exact prefixes at breakpoints. By default it places one implicit breakpoint &lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching#caching-behavior-changes-when-migrating-to-gpt-56" rel="noopener noreferrer"&gt;at the latest user or tool message&lt;/a&gt;, and it no longer falls back to the longest matching prefix before that point. A request can therefore share thousands of identical tokens with the previous one, report zero cached tokens, and pay to write the changing prefix again.&lt;/p&gt;

&lt;p&gt;The lever Spring AI gives you is the cache key. Requests that share a prefix should carry the same one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;
&lt;span class="nc"&gt;OpenAiChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAiChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5.6-terra"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;promptCacheKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"support-assistant-v1"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html#_configuration_properties" rel="noopener noreferrer"&gt;&lt;code&gt;spring.ai.openai.chat.prompt-cache-key=support-assistant-v1&lt;/code&gt;&lt;/a&gt;. On GPT-5.6 and later, this key is required for the more reliable matching. Keep traffic per key to roughly &lt;a href="https://developers.openai.com/api/docs/guides/prompt-caching#improve-cache-hit-rates-with-a-prompt-cache-key" rel="noopener noreferrer"&gt;15 requests per minute&lt;/a&gt; and partition busier workloads across several keys.&lt;/p&gt;

&lt;p&gt;Spring AI 2.0 does not expose explicit cache breakpoints, so on OpenAI the cacheable prefix is whatever your prompt structure gives you — which makes the static-first rule below a cost control, not a style preference. It also reports cache reads &lt;a href="https://docs.spring.io/spring-ai/reference/upgrade-notes.html#_unified_cache_usage_metrics_on_usage" rel="noopener noreferrer"&gt;but not cache writes for OpenAI&lt;/a&gt;, so the 1.25× write charge is invisible in your token metrics. Watch it on the provider's dashboard instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local models (Ollama): free, but still worth planning
&lt;/h3&gt;

&lt;p&gt;Local models also use a cache, but it saves time, not money. Before a model can answer, it must first process every token of the prompt — an expensive "reading" step on the GPU. The engine keeps the result of this step in memory (the KV cache). If the next request begins with exactly the same text, the engine skips that part and processes only what is new. The result: a faster first token and more requests per GPU. But nothing on a bill gets smaller, because locally there are no per-token charges. The cache also lives only while the model is loaded — Ollama unloads idle models after five minutes, though &lt;code&gt;keep_alive&lt;/code&gt; can hold them in memory for longer.&lt;/p&gt;

&lt;p&gt;Note that the cache only matches from the start of the prompt: at the first token that differs, everything after it is processed again. So the static-first rule applies here too.&lt;/p&gt;

&lt;h3&gt;
  
  
  The general rule: static content first, changing content last
&lt;/h3&gt;

&lt;p&gt;Provider prompt caching mechanisms depend on matching the beginning of the prompt. A cache hit requires an identical prefix, so fixed content — instructions, examples, and tool definitions — should be placed before changing content. A single timestamp placed at the top of the system prompt can invalidate the cached prefix that follows it. Spring AI 2.0 supports separating static and dynamic &lt;code&gt;SystemMessage&lt;/code&gt; blocks for providers that support multi-block system caching, such as &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/anthropic-chat.html#_multi_block_system_caching" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt; and &lt;a href="https://docs.spring.io/spring-ai/reference/api/chat/bedrock-converse.html#_multi_block_system_message_caching" rel="noopener noreferrer"&gt;AWS Bedrock Converse&lt;/a&gt;. With &lt;code&gt;.multiBlockSystemCaching(true)&lt;/code&gt;, Spring AI can preserve cacheable system blocks while allowing later changing system content to remain outside the cached prefix.&lt;/p&gt;

&lt;p&gt;Structure your prompts this way even before you turn caching on — it is what makes every provider's cache, automatic or explicit, actually work for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;There is a limit to how far prompt structure can take you, though. Everything in this part assumed the content was yours: your system prompt, your conversation, your examples. &lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9"&gt;The next part&lt;/a&gt; deals with the context your application adds automatically — document chunks from a vector store and tool definitions from every server you connect to. Both arrive as input tokens, both are sent whether the model uses them or not, and both scale with how much you have indexed rather than with how much you need.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Spring AI Token Usage: Measure Cost Before You Pick a Model — LLM Cost Control 1/4</title>
      <dc:creator>Julia Denysova</dc:creator>
      <pubDate>Thu, 30 Jul 2026 15:26:31 +0000</pubDate>
      <link>https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo</link>
      <guid>https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo</guid>
      <description>&lt;p&gt;&lt;em&gt;Cutting LLM costs in Spring AI starts with two choices: which model answers a request, and what defaults your &lt;code&gt;ChatClient&lt;/code&gt; adds to every one it sends.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Neither is worth changing until you can see where the tokens go. That is why this article starts with measurement.&lt;/p&gt;

&lt;p&gt;This is Part 1 of four, and it covers the first three of ten cost drivers. Driver #0 tells you where the money actually goes; #1 and #2 are the two decisions that shape every request your application sends. The remaining seven attach to what you build here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A note on the numbers: where a price ratio matters for the argument (input vs. output, cache read vs. write), this series quotes real August 2026 list prices with a link. All other examples use a flat rate of $1 per million input tokens, so you can redo the calculation with your own provider's price sheet. You should do that, because these prices change every few months.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Driver #0 — Spring AI observability measurement: you cannot cut what you cannot see
&lt;/h2&gt;

&lt;p&gt;Provider invoices and usage dashboards usually show your spending by model and by token type — input, output, and cached. That is useful, but it is not enough. The numbers cannot tell you which feature, client, or advisor inside your application was responsible for that usage.&lt;/p&gt;

&lt;p&gt;Spring AI integrates with &lt;a href="https://docs.spring.io/spring-ai/reference/observability/index.html" rel="noopener noreferrer"&gt;Spring Boot's Micrometer-based observability&lt;/a&gt; to fill this gap. Its core AI components automatically emit that data. &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/model/ChatModel.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatModel&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/embedding/EmbeddingModel.html" rel="noopener noreferrer"&gt;&lt;code&gt;EmbeddingModel&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/image/ImageModel.html" rel="noopener noreferrer"&gt;&lt;code&gt;ImageModel&lt;/code&gt;&lt;/a&gt; implementations (support varies by provider) publish model-level observations, including token usage where available. &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/client/ChatClient.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatClient&lt;/code&gt;&lt;/a&gt; (including advisors) and &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/vectorstore/VectorStore.html" rel="noopener noreferrer"&gt;&lt;code&gt;VectorStore&lt;/code&gt;&lt;/a&gt; report execution observations and traces rather than token usage metrics.&lt;/p&gt;

&lt;p&gt;Each metric includes built-in tags, such as the model name and token type. These tags separate models and providers, but not callers: every request to the same model carries the same tag values, so they cannot tell two features apart on their own. Spring AI marks tags as low- or high-cardinality: low-cardinality tags go on both metrics and traces, high-cardinality tags go on traces only. To attribute usage to a &lt;a href="https://docs.spring.io/spring-framework/reference/integration/observability.html#observability.config.conventions" rel="noopener noreferrer"&gt;specific feature or client&lt;/a&gt;, add your own low-cardinality tags. Keep those values few and stable, not one per user or request. For more on how tags work, and how to keep their number under control, see &lt;a href="https://docs.micrometer.io/micrometer/reference/concepts/naming.html" rel="noopener noreferrer"&gt;Micrometer's documentation on tags and naming&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With this data in place, you can answer the questions the later drivers raise. Is history growth (&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2&lt;/a&gt;) really increasing costs? Watch prompt token usage over time. Did the tool-search advisor (&lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9"&gt;Part 3&lt;/a&gt;) reduce the amount of context sent to the model? Compare prompt token usage before and after enabling it. Is a reasoning model becoming more expensive than expected (&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2&lt;/a&gt;)? Output token trends can reveal unexpectedly long responses and rising generation costs.&lt;/p&gt;

&lt;p&gt;Two more steps make this setup safer, not just more informative. First, create an alert for sudden increases in token usage or request volume. A bug that puts an LLM call inside a loop can burn through your budget fast, and an alert catches it within minutes instead of at the end of the month. Second, configure provider-side spending controls where available, such as budgets, quotas, or usage limits. This is a final safeguard that no application framework can enforce.&lt;/p&gt;

&lt;p&gt;Treat observability as step zero. Set it up before optimising anything else, so every later change has a measurable before-and-after comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #1 — Model choice: stop paying flagship prices for mini-model work
&lt;/h2&gt;

&lt;p&gt;The fastest way to overspend on an LLM application is to send every request to the most capable model you can find. Flagship and budget models on the same provider's price sheet can differ tenfold in price per token, or more. But a large share of typical workloads — classification, extraction, routing, short summaries — passes the quality bar on the smaller model already. Sending those requests to a flagship model increases cost without improving the outcome.&lt;/p&gt;

&lt;p&gt;Price is only one factor, though. Models also differ in what they can do: some accept images or audio, some are built for tool calling or strict structured output, some reason step by step while others just answer directly. The rule is the same either way: pick the smallest model that covers what the task actually needs. Do not pay for image support on a text-only pipeline, and do not pay for a reasoning model just to reformat JSON. Requirements change over time, so treat your model choice as temporary, not final. Providers release cheaper and better models every few months, and a task that needed the flagship model yesterday often works fine on the mini model next quarter.&lt;/p&gt;

&lt;p&gt;This only pays off if switching models is easy. Spring AI's answer is that the model is configuration, not architecture. &lt;code&gt;ChatClient&lt;/code&gt; does not depend on any one provider or model: your service code does not need to know which model answers, portable options like &lt;code&gt;maxTokens&lt;/code&gt; and &lt;code&gt;temperature&lt;/code&gt; carry over between models, and provider-specific settings stay isolated in one options object. &lt;a href="https://docs.spring.io/spring-ai/reference/index.html" rel="noopener noreferrer"&gt;Spring AI 2.0&lt;/a&gt; makes this explicit: default options set on a &lt;code&gt;ChatClient&lt;/code&gt; act as a partial "delta" over the model's start-up configuration. The framework signals this same idea in its own defaults: the OpenAI integration in 2.0 &lt;a href="https://spring.io/blog/2025/12/11/spring-ai-2-0-0-M1-available-now#key-functional-areas-enhanced" rel="noopener noreferrer"&gt;defaults to &lt;code&gt;gpt-5-mini&lt;/code&gt;&lt;/a&gt;, not the flagship model. A good pattern is one client per cost tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChatClients&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nd"&gt;@Primary&lt;/span&gt;
    &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;cheapClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5-mini"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;flagshipClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ticket classifier feature could inject &lt;code&gt;@Qualifier("cheapClient") ChatClient&lt;/code&gt;, while a contract-analysis feature injects the flagship one. Two details matter here. First, build clients from the auto-configured builder, not from a raw &lt;code&gt;ChatClient.builder(chatModel)&lt;/code&gt;. The auto-configured builder already has observability wired in, and the token metrics from Driver #0 depend on that. Second, this example covers two tiers on one provider. If you mix vendors in the same application, create a separate &lt;code&gt;ChatClient&lt;/code&gt; from each auto-configured &lt;code&gt;ChatModel&lt;/code&gt; instead. Either way, the size of the change stays small: swapping models is a property change, and swapping vendors just adds a starter dependency plus properties. No code changes, no prompt rewrites, no new SDK to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Driver #2 — One shared client: every request carries every default
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;ChatClient&lt;/code&gt; default is not free. Everything attached to the client — system prompt, tools, memory advisor — is included in every request that client sends. One shared "do-everything" client means a simple 10-token call (e.g. a classifier feature) carries the same 2,000-token system prompt, tool schemas, and conversation history as your most complex feature. If the classifier runs 50,000 times a month, that is 100 million input tokens of extra content it never actually needs — $100 a month at the example rate, for nothing.&lt;/p&gt;

&lt;p&gt;Spring AI's unit of cost control is the client, not the whole application. &lt;a href="https://docs.spring.io/spring-ai/reference/api/chatclient.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatClient.Builder&lt;/code&gt;&lt;/a&gt; is auto-configured as a prototype bean, so each task can build its own client with only the defaults that task needs: &lt;code&gt;.defaultSystem()&lt;/code&gt; for the prompt, &lt;code&gt;.defaultOptions()&lt;/code&gt; for model and token settings, &lt;code&gt;.defaultAdvisors()&lt;/code&gt; and &lt;code&gt;.defaultTools()&lt;/code&gt; for the rest. Every later driver in this series — memory windows, RAG advisors, tool search — is attached at exactly this level. This is why building per-task clients should come before the drivers that follow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskClients&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;classifierClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Classify the ticket. Reply with one word: BILLING, TECH, or OTHER."&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5-mini"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="nc"&gt;ChatClient&lt;/span&gt; &lt;span class="nf"&gt;supportChatClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ChatMemory&lt;/span&gt; &lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultSystem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ClassPathResource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"prompts/support.st"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultOptions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-5"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultAdvisors&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MessageChatMemoryAdvisor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memory&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note the &lt;a href="https://docs.spring.io/spring-ai/reference/upgrade-notes.html" rel="noopener noreferrer"&gt;Spring AI 2.0 change&lt;/a&gt;: &lt;code&gt;.defaultOptions()&lt;/code&gt; and the per-call &lt;code&gt;.options()&lt;/code&gt; now take a &lt;code&gt;ChatOptions.Builder&lt;/code&gt;, not a fully built instance. This builder is merged with the model's defaults before the first advisor runs, and only the fields you explicitly set override them. That merge lets you override only the options you need without replacing the model's configured defaults:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;classifierClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// override for a yes/no case&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: each request pays only for the defaults its own task actually declared. That is a requirement for every control that follows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Drivers #1 and #2 decide which model answers a request and which client sends it; Driver #0 shows you what those decisions cost. You now have one client per task, each carrying only the defaults that task needs, and the token metrics to prove it.&lt;/p&gt;

&lt;p&gt;None of that limits the size of a single request. A reasoning model can bill more hidden thinking than visible answer. A support conversation resends its whole history on every turn. The same system prompt goes out thousands of times a day at full price, unless you structure it so the provider can cache it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2&lt;/a&gt; covers those three. Each one is a limit or an advisor you attach to the clients you have just built. &lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>How to Reduce LLM Costs in Spring AI 2.0: 10 Practical Controls</title>
      <dc:creator>Julia Denysova</dc:creator>
      <pubDate>Thu, 30 Jul 2026 15:26:02 +0000</pubDate>
      <link>https://dev.to/julia_denysova/how-to-reduce-llm-costs-in-spring-ai-20-10-practical-controls-2jj7</link>
      <guid>https://dev.to/julia_denysova/how-to-reduce-llm-costs-in-spring-ai-20-10-practical-controls-2jj7</guid>
      <description>&lt;p&gt;&lt;em&gt;Spring AI's defaults are built for a fast start; they do not guarantee a low monthly cost. Shipping an LLM feature is easy — making it cost-efficient is not. This series shows the spots where money leaks, along with the control that closes each one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://spring.io/blog/2026/06/12/spring-ai-2-0-0-GA-available-now" rel="noopener noreferrer"&gt;Spring AI 2.0 reached GA on 12 June 2026&lt;/a&gt;. It needs &lt;a href="https://docs.spring.io/spring-boot/index.html" rel="noopener noreferrer"&gt;Spring Boot 4&lt;/a&gt;, moves the tool-calling loop out of the &lt;a href="https://docs.spring.io/spring-ai/docs/current/api/org/springframework/ai/chat/model/ChatModel.html" rel="noopener noreferrer"&gt;&lt;code&gt;ChatModel&lt;/code&gt;&lt;/a&gt;, adds tool search, and extends structured outputs. Tool search and the extended structured-output controls point in the same direction: they determine how many tokens your application sends and receives.&lt;/p&gt;

&lt;p&gt;The bill grows quietly. A chatbot with a 2,000-token system prompt, run 100,000 times a month, sends 200 million tokens of the same text. At an example rate of $1 per million input tokens, that is $200 a month — before a single user message. Then add conversation history, which is sent in full on every turn. Add retrieved RAG documents and the JSON schema of every registered tool. The input side can grow 10× with no change in traffic at all. Output tokens cost several times more per token than input tokens, and reasoning models bill their hidden "thinking" as output too.&lt;/p&gt;

&lt;p&gt;The provider sets the prices. The framework gives you controls that can reduce the number of tokens you pay for.&lt;/p&gt;

&lt;p&gt;This series works through ten cost drivers, numbered #0 to #9. Each one is a place where tokens repeat or grow without anyone deciding they should, and each comes with the Spring AI control that cuts it. They are spread across four parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Part 1 — Token Usage: Measure Cost Before You Pick a Model (Drivers #0–#2)&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Provider dashboards show what you spent, but not which feature spent it. Spring AI's observability closes that gap, and from there you can match each model to its task and stop features from carrying defaults they never needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/julia_denysova/spring-ai-prompt-caching-and-chat-memory-where-the-tokens-go-llm-cost-control-24-36i"&gt;Part 2 — Prompt Caching and Chat Memory: Where the Tokens Go (Drivers #3–#5)&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This part covers limiting response length, bounding how much conversation history you resend, and structuring prompts so that provider caching actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/julia_denysova/spring-ai-rag-and-tool-calling-paying-for-context-you-dont-use-llm-cost-control-34-2ia9"&gt;Part 3 — RAG and Tool Calling: Paying for Context You Don't Use (Drivers #6–#7)&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Retrieved documents and tool definitions are added to every request, whether the model needs them or not. This part covers retrieving less, cleaning up what was retrieved, and sending tool schemas only when they are relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/julia_denysova/spring-ai-retries-and-embeddings-failed-answers-and-full-re-indexes-llm-cost-control-44-3i53"&gt;Part 4 — Retries and Embeddings: Failed Answers and Full Re-indexes (Drivers #8–#9)&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A failed response is billed in full, and retrying it resends the entire context. This part covers preventing malformed output at the provider level, then the indexing side: embedding model choice, vector size, batching, and re-embedding only what has changed.&lt;/p&gt;




&lt;p&gt;Start with &lt;a href="https://dev.to/julia_denysova/spring-ai-token-usage-measure-cost-before-you-pick-a-model-llm-cost-control-14-41fo"&gt;Part 1&lt;/a&gt;. Every later driver asks you to trade something — context, memory, response length — and per-feature metrics are what tell you which trade is worth making. Part 1 also covers the cheapest change available: sending routine work to a smaller model, which usually needs no more than a property change.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
