<?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: Hassan Imam</title>
    <description>The latest articles on DEV Community by Hassan Imam (@hassan_shah_733ea1eb37c88).</description>
    <link>https://dev.to/hassan_shah_733ea1eb37c88</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%2F3919991%2F2abd43d0-6aa6-47be-a38a-ca0e563dc2d5.jpg</url>
      <title>DEV Community: Hassan Imam</title>
      <link>https://dev.to/hassan_shah_733ea1eb37c88</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hassan_shah_733ea1eb37c88"/>
    <language>en</language>
    <item>
      <title>Two AI debuggers found the same bug. Both stopped one function short.</title>
      <dc:creator>Hassan Imam</dc:creator>
      <pubDate>Sat, 22 Aug 2026 02:31:33 +0000</pubDate>
      <link>https://dev.to/hassan_shah_733ea1eb37c88/two-ai-debuggers-found-the-same-bug-both-stopped-one-function-short-n10</link>
      <guid>https://dev.to/hassan_shah_733ea1eb37c88/two-ai-debuggers-found-the-same-bug-both-stopped-one-function-short-n10</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ChatGoogleGenerativeAI&lt;/code&gt; computed your Gemini token counts correctly, then threw&lt;br&gt;
half of them away. Any cost tracker built on LangChain's callback system read&lt;br&gt;
&lt;strong&gt;zero tokens&lt;/strong&gt; on every call.&lt;/p&gt;

&lt;p&gt;I reproduced it on the current release, fixed it, and shipped&lt;br&gt;
&lt;a href="https://github.com/langchain-ai/langchain-google/pull/1954" rel="noopener noreferrer"&gt;PR #1954&lt;/a&gt; to&lt;br&gt;
&lt;code&gt;langchain-google&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part: I pointed two AI debugging tools at the same code — Google's&lt;br&gt;
Gemini CLI and Sentry's Seer. Both correctly identified the same function. Seer&lt;br&gt;
even independently validated the trickiest design decision in the patch. &lt;strong&gt;Neither&lt;br&gt;
of them found the second half of the fix&lt;/strong&gt; — and without it, the bug is still there.&lt;/p&gt;


&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langchain-google" rel="noopener noreferrer"&gt;&lt;code&gt;langchain-google-genai&lt;/code&gt;&lt;/a&gt; is&lt;br&gt;
the official LangChain integration for Google's Gemini models — the package you&lt;br&gt;
install to use &lt;code&gt;ChatGoogleGenerativeAI&lt;/code&gt; in a LangChain app. It lives in the&lt;br&gt;
&lt;code&gt;langchain-google&lt;/code&gt; monorepo alongside the Vertex AI and community packages, and&lt;br&gt;
ships roughly every couple of weeks.&lt;/p&gt;

&lt;p&gt;Token counts are not a nice-to-have in that stack. They are how you bill&lt;br&gt;
customers, enforce per-tenant quotas, populate observability dashboards, and&lt;br&gt;
notice that a prompt change quietly tripled your spend. LangChain exposes them in&lt;br&gt;
two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AIMessage.usage_metadata&lt;/code&gt; — the modern, per-message standard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LLMResult.llm_output["token_usage"]&lt;/code&gt; — the older convention that the
&lt;strong&gt;callback system&lt;/strong&gt; reads, and that every other chat integration still populates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Callbacks are what production apps hook into, because they fire for every call&lt;br&gt;
without touching business logic. Which is exactly what made this bug expensive:&lt;br&gt;
the counts were right, the callbacks read zero, and nothing raised an error.&lt;/p&gt;


&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The bug
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/langchain-ai/langchain-google/issues/957" rel="noopener noreferrer"&gt;Issue #957&lt;/a&gt; reports&lt;br&gt;
that &lt;code&gt;BaseCallbackHandler.on_llm_end&lt;/code&gt; sees no token usage for&lt;br&gt;
&lt;code&gt;ChatGoogleGenerativeAI&lt;/code&gt;. The issue was closed with no linked PR and no fix commit,&lt;br&gt;
and the package was rewritten onto the unified &lt;code&gt;google-genai&lt;/code&gt; SDK in 4.0.0 — so my&lt;br&gt;
first job was to find out whether the bug still existed at all. In other words: a&lt;br&gt;
bug the tracker considered resolved, in a package that had since been rewritten&lt;br&gt;
from the ground up.&lt;/p&gt;

&lt;p&gt;It does. Here it is on &lt;code&gt;langchain-google-genai&lt;/code&gt; &lt;strong&gt;4.3.4&lt;/strong&gt;, the current release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenProbe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseCallbackHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_llm_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LLMResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_output:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message.usage_metadata:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage_metadata&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatGoogleGenerativeAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;TokenProbe&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="nc"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Say hello in 3 words.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;llm_output&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;message.usage_metadata:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;'input_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'output_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;592&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'total_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counts exist. They're just not where callback-based token tracking looks for&lt;br&gt;
them. Every &lt;code&gt;get_openai_callback&lt;/code&gt;-style cost tracker, every LangSmith-style&lt;br&gt;
accounting layer reading &lt;code&gt;llm_output["token_usage"]&lt;/code&gt;, reports zero for Gemini.&lt;/p&gt;


&lt;h3&gt;
  
  
  Root cause
&lt;/h3&gt;

&lt;p&gt;It's in &lt;code&gt;libs/genai/langchain_google_genai/chat_models.py&lt;/code&gt;, in&lt;br&gt;
&lt;code&gt;_response_to_result&lt;/code&gt;. The function builds &lt;code&gt;llm_output&lt;/code&gt; at line 1298:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_feedback&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_feedback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_feedback&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it computes the token counts into &lt;code&gt;lc_usage&lt;/code&gt;, attaches them to the message&lt;br&gt;
at line 1404:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and returns at line 1456:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ChatResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;llm_output&lt;/code&gt; is never touched in between. &lt;code&gt;grep&lt;/code&gt; confirms it: the identifier&lt;br&gt;
appears at exactly &lt;strong&gt;two lines in the entire file&lt;/strong&gt; — where it's created and where&lt;br&gt;
it's returned.&lt;/p&gt;

&lt;p&gt;The package migrated to the modern &lt;code&gt;usage_metadata&lt;/code&gt; standard on the message and&lt;br&gt;
left the &lt;code&gt;llm_output["token_usage"]&lt;/code&gt; convention — which every other LangChain chat&lt;br&gt;
integration still populates, and which the callback API still reads — empty.&lt;/p&gt;


&lt;h3&gt;
  
  
  Why the obvious fix is incomplete
&lt;/h3&gt;

&lt;p&gt;This is the part both AI tools missed, and it's the reason a one-line patch&lt;br&gt;
doesn't work.&lt;/p&gt;

&lt;p&gt;Populating &lt;code&gt;llm_output&lt;/code&gt; inside &lt;code&gt;_response_to_result&lt;/code&gt; fixes the &lt;strong&gt;callback&lt;/strong&gt; path.&lt;br&gt;
It does &lt;strong&gt;not&lt;/strong&gt; fix the value you get back from &lt;code&gt;generate()&lt;/code&gt;. Here's why, from&lt;br&gt;
&lt;code&gt;langchain_core/language_models/chat_models.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;flattened_outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;LLMResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;llm_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_combine_llm_outputs&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLMResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;generations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;run_managers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flattened_output&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_managers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flattened_outputs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_llm_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flattened_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;- gets res.llm_output DIRECTLY
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;                                  &lt;span class="c1"&gt;# &amp;lt;- built via _combine_llm_outputs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different objects. &lt;code&gt;on_llm_end&lt;/code&gt; receives &lt;code&gt;flattened_output&lt;/code&gt;, built from&lt;br&gt;
&lt;code&gt;res.llm_output&lt;/code&gt; directly. The &lt;strong&gt;return value&lt;/strong&gt; is built through&lt;br&gt;
&lt;code&gt;_combine_llm_outputs&lt;/code&gt; — whose &lt;code&gt;BaseChatModel&lt;/code&gt; default is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_combine_llm_outputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_llm_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ChatGoogleGenerativeAI&lt;/code&gt; never overrode it. So patch only&lt;br&gt;
&lt;code&gt;_response_to_result&lt;/code&gt; and &lt;code&gt;result.llm_output&lt;/code&gt; is &lt;em&gt;still&lt;/em&gt; &lt;code&gt;{}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My reproduction printed both paths, which is the only reason I noticed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Two changes. First, mirror the counts into &lt;code&gt;llm_output&lt;/code&gt; using the same key&lt;br&gt;
convention as the other integrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completion_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lc_usage&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note it sources from &lt;code&gt;lc_usage&lt;/code&gt;, not the raw response. &lt;code&gt;lc_usage&lt;/code&gt; has already had&lt;br&gt;
&lt;code&gt;prev_usage&lt;/code&gt; subtracted, so on the streaming path each chunk carries its own delta&lt;br&gt;
— consistent with how &lt;code&gt;message.usage_metadata&lt;/code&gt; already behaves.&lt;/p&gt;

&lt;p&gt;Second, the override that makes the return value work, mirroring&lt;br&gt;
&lt;code&gt;langchain-openai&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_combine_llm_outputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_outputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;llm_outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token_usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token_usage&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;llm_output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;'token_usage':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;'prompt_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'completion_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;775&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;'total_tokens':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;783&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="err"&gt;'usage_metadata':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'model_name':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'gemini&lt;/span&gt;&lt;span class="mf"&gt;-2.5&lt;/span&gt;&lt;span class="err"&gt;-flash'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;RESULT.llm_output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;'model_name':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'gemini&lt;/span&gt;&lt;span class="mf"&gt;-2.5&lt;/span&gt;&lt;span class="err"&gt;-flash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'token_usage':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwk5yd0od90rj2on8svf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwk5yd0od90rj2on8svf.png" alt="The PR diff showing both changes — the token_usage block in _response_to_result and the _combine_llm_outputs override" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deliberately out of scope:&lt;/strong&gt; streaming aggregation. &lt;code&gt;_response_to_result&lt;/code&gt; is&lt;br&gt;
shared with the streaming path, and the new &lt;code&gt;llm_output&lt;/code&gt; carries the same&lt;br&gt;
per-chunk delta as &lt;code&gt;message.usage_metadata&lt;/code&gt;, so the two stay consistent. Changing&lt;br&gt;
how chunks &lt;em&gt;aggregate&lt;/em&gt; is a separate problem with real double-counting risk&lt;br&gt;
(see langchainjs #8266) and deserves its own PR.&lt;/p&gt;


&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Tests and CI
&lt;/h3&gt;

&lt;p&gt;Four unit tests covering both sites plus the no-usage-metadata edge case. They&lt;br&gt;
assert the invariant that matters — that &lt;code&gt;llm_output&lt;/code&gt; agrees with the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;usage_metadata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full suite: &lt;strong&gt;377 passing&lt;/strong&gt;, no regressions. &lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;ruff format&lt;/code&gt; and &lt;code&gt;mypy&lt;/code&gt;&lt;br&gt;
clean across all 35 source files. CI green on &lt;strong&gt;Python 3.10, 3.11, 3.12, 3.13&lt;br&gt;
and 3.14&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fxqcsd4wl3vi87ikpo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fxqcsd4wl3vi87ikpo6.png" alt="GitHub CI checks all passing: make lint on Python 3.10 and 3.14, make test on 3.10 through 3.14" width="798" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w2uafz0q0bcmy7qgfho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7w2uafz0q0bcmy7qgfho.png" alt="Pull request 1954 on langchain-ai/langchain-google, 149 additions and 1 deletion across 2 files" width="800" height="1125"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;Two AI tools, different vendors, different architectures, different access —&lt;br&gt;
Seer had my actual repository — reached the same correct-but-partial answer. Both&lt;br&gt;
found the function where the tokens &lt;em&gt;should&lt;/em&gt; be written. Neither traced what&lt;br&gt;
happens to &lt;code&gt;llm_output&lt;/code&gt; after &lt;code&gt;_response_to_result&lt;/code&gt; returns.&lt;/p&gt;

&lt;p&gt;That's not a knock on either. Both were genuinely useful, and Seer's streaming&lt;br&gt;
observation was sharper than I expected. But the gap is instructive: they answered&lt;br&gt;
the question as posed — &lt;em&gt;why is &lt;code&gt;llm_output&lt;/code&gt; empty?&lt;/em&gt; — and stopped. They didn't&lt;br&gt;
ask the follow-up: &lt;em&gt;does fixing that actually make the value reach the caller?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The reproduction script is what caught it, because it printed both paths and only&lt;br&gt;
one of them changed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;I ran the instrumented probe against the unpatched fork and against the patched&lt;br&gt;
one, tagging each run &lt;code&gt;before&lt;/code&gt; / &lt;code&gt;after&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error monitoring.&lt;/strong&gt; A cost-tracker assertion failing on the empty &lt;code&gt;llm_output&lt;/code&gt;,&lt;br&gt;
captured across eight runs. Each event carries the package versions and a context&lt;br&gt;
block naming the suspect file and function, so the issue page alone tells you&lt;br&gt;
where to look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bjb6j170bb5kamgfb9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bjb6j170bb5kamgfb9f.png" alt="Sentry issues list showing TokenAccountingError with 8 events, culprit gemini-token-probe-before" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgl8xnmmjbrmgt15xl2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgl8xnmmjbrmgt15xl2v.png" alt="Sentry issue detail showing tags for langchain-core 1.6.0 and langchain-google-genai 4.3.4, plus the issue 957 context block naming the suspect file and function" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracing.&lt;/strong&gt; The &lt;code&gt;gen_ai.chat&lt;/code&gt; span, before and after:&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;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens&lt;/td&gt;
&lt;td&gt;8 in + 29 out = &lt;strong&gt;37 total&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;8 in + 741 out = &lt;strong&gt;749 total&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issues on trace&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr0y94d409h38e6k83brw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr0y94d409h38e6k83brw.png" alt="Sentry trace before the fix: gen_ai.chat span showing 8 in, 29 out, 37 total tokens, with 1 issue on the trace" width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkg2zkipnq3evdnyw5htg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkg2zkipnq3evdnyw5htg.png" alt="Sentry trace after the fix: gen_ai.chat span showing 8 in, 741 out, 749 total tokens, with 0 issues on the trace" width="799" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftnfaps0ysubayesa0z9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftnfaps0ysubayesa0z9c.png" alt="Sentry error list filtered to environment after, showing no issues match the search" width="799" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent monitoring and conversation traces.&lt;/strong&gt; Because the probe ran with&lt;br&gt;
&lt;code&gt;include_prompts=True&lt;/code&gt;, Sentry captured the full exchange on the &lt;code&gt;gen_ai.chat&lt;/code&gt;&lt;br&gt;
span — prompt, completion, model, latency, cost and token breakdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F62xg0iint8o4h9ufnffs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F62xg0iint8o4h9ufnffs.png" alt="Sentry gen_ai.chat span Output tab showing the response " width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one span makes the cost argument better than any explanation of mine. The&lt;br&gt;
prompt was &lt;em&gt;"Say hello in 3 words."&lt;/em&gt; The response was &lt;strong&gt;"Hello, my friend."&lt;/strong&gt;&lt;br&gt;
Three words. It cost &lt;strong&gt;749 tokens&lt;/strong&gt;, because &lt;code&gt;gemini-2.5-flash&lt;/code&gt; spent &lt;strong&gt;736&lt;/strong&gt; of&lt;br&gt;
them reasoning before answering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;'output_token_details':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;'reasoning':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;736&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is exactly the kind of spend you cannot eyeball from the response text, and&lt;br&gt;
exactly what token accounting exists to surface. Before this fix, a&lt;br&gt;
&lt;code&gt;BaseCallbackHandler&lt;/code&gt;-based cost tracker watching that call reported zero tokens&lt;br&gt;
and zero cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And here's where I have to be honest about something.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I expected to write "Sentry showed zero Gemini tokens." It would have been a&lt;br&gt;
great line. It's also false, and my own traces prove it — the token counts are&lt;br&gt;
correct in &lt;em&gt;both&lt;/em&gt; runs.&lt;/p&gt;

&lt;p&gt;I checked the SDK source before publishing. &lt;code&gt;sentry_sdk/integrations/langchain.py&lt;/code&gt;&lt;br&gt;
resolves tokens like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;possible_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_usage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# checked FIRST
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;possible_names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;llm_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm_output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# only if message had nothing
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sentry checks the &lt;strong&gt;message&lt;/strong&gt; before &lt;code&gt;llm_output&lt;/code&gt;, and falls back to&lt;br&gt;
&lt;code&gt;_extract_tokens_from_generations&lt;/code&gt; when the &lt;code&gt;LLMResult&lt;/code&gt; has nothing. In other&lt;br&gt;
words: &lt;strong&gt;Sentry ships a defensive workaround for exactly this class of bug.&lt;/strong&gt; It&lt;br&gt;
reports Gemini tokens correctly &lt;em&gt;despite&lt;/em&gt; the defect, because it doesn't trust&lt;br&gt;
&lt;code&gt;llm_output&lt;/code&gt; to be populated.&lt;/p&gt;

&lt;p&gt;That's a better story than the one I expected to tell. The fix doesn't repair&lt;br&gt;
Sentry's numbers — it removes the need for the workaround.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs.&lt;/strong&gt; Structured logs recording &lt;code&gt;llm_output&lt;/code&gt; on every &lt;code&gt;on_llm_end&lt;/code&gt;, empty&lt;br&gt;
before and fully populated after.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrhsk9v9bgjofvwki4xp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrhsk9v9bgjofvwki4xp.png" alt="Sentry structured logs under environment before, showing on_llm_end llm_output equals an empty dict" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwd0rotaw85za4kdumg2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwd0rotaw85za4kdumg2.png" alt="Sentry structured logs under environment after, showing on_llm_end llm_output populated with token_usage, usage_metadata and model_name" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seer.&lt;/strong&gt; Sentry's AI debugger, connected to my fork. It read &lt;code&gt;chat_models.py&lt;/code&gt;&lt;br&gt;
straight from the repo, identified &lt;code&gt;_response_to_result&lt;/code&gt;, and produced a&lt;br&gt;
two-step plan. Step 2 said to use the delta-adjusted &lt;code&gt;lc_usage&lt;/code&gt; rather than the&lt;br&gt;
raw response values, so streaming chunks stay correct — independently arriving at&lt;br&gt;
the same conclusion I'd flagged for reviewers in the PR description.&lt;/p&gt;

&lt;p&gt;Its cited evidence covers &lt;code&gt;chat_models.py&lt;/code&gt; L1292–L1489. &lt;code&gt;_combine_llm_outputs&lt;/code&gt;&lt;br&gt;
lives at L3370. It never got there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vxcgjqa4f7sfjn8250u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9vxcgjqa4f7sfjn8250u.png" alt="Sentry Seer root cause analysis and solution plan, showing it read chat_models.py from the fork and identified _response_to_result" width="800" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One practical note for anyone instrumenting LangChain: &lt;strong&gt;it swallows exceptions&lt;br&gt;
raised inside callbacks.&lt;/strong&gt; My first probe asserted inside &lt;code&gt;on_llm_end&lt;/code&gt; and nothing&lt;br&gt;
ever reached Sentry. Assert on the returned &lt;code&gt;LLMResult&lt;/code&gt; instead.&lt;/p&gt;


&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemini CLI as an agentic bisect.&lt;/strong&gt; I ran &lt;code&gt;@google/gemini-cli&lt;/code&gt; v0.56.0 with&lt;br&gt;
&lt;code&gt;gemini-2.5-flash&lt;/code&gt; against the &lt;em&gt;unpatched&lt;/em&gt; tree, so the diagnosis couldn't be&lt;br&gt;
contaminated by my fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gemini &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;prompt&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--model&lt;/span&gt; gemini-2.5-flash &lt;span class="nt"&gt;--skip-trust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It found &lt;code&gt;_response_to_result&lt;/code&gt;, cited lines 1298–1302 for the initialization,&lt;br&gt;
1404 for &lt;code&gt;message.usage_metadata = lc_usage&lt;/code&gt;, and 1456 for the return, then&lt;br&gt;
proposed &lt;code&gt;token_usage&lt;/code&gt; / &lt;code&gt;usage_metadata&lt;/code&gt; / &lt;code&gt;model_name&lt;/code&gt; with the correct key&lt;br&gt;
names — essentially the patch I'd written, at the same insertion point.&lt;/p&gt;

&lt;p&gt;It did not mention &lt;code&gt;_combine_llm_outputs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini API as a correctness oracle.&lt;/strong&gt; Self-consistency isn't enough — matching&lt;br&gt;
numbers could both be wrong. So I validated the patched &lt;code&gt;llm_output&lt;/code&gt; against the&lt;br&gt;
raw &lt;code&gt;google-genai&lt;/code&gt; SDK's own &lt;code&gt;prompt_token_count&lt;/code&gt; for the same prompt, which is&lt;br&gt;
deterministic for a fixed model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prompt                                                SDK in   LC in  match   sums
Say hello in 3 words.                                      8       8   PASS   PASS
Name one primary color. Answer with a single word.        12      12   PASS   PASS
Reply with exactly the word: ok                            8       8   PASS   PASS

RESULT: 3/3 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PR:&lt;/strong&gt; &lt;a href="https://github.com/langchain-ai/langchain-google/pull/1954" rel="noopener noreferrer"&gt;https://github.com/langchain-ai/langchain-google/pull/1954&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue:&lt;/strong&gt; &lt;a href="https://github.com/langchain-ai/langchain-google/issues/957" rel="noopener noreferrer"&gt;https://github.com/langchain-ai/langchain-google/issues/957&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fork:&lt;/strong&gt; &lt;a href="https://github.com/hassan-2050/langchain-google" rel="noopener noreferrer"&gt;https://github.com/hassan-2050/langchain-google&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Environment: &lt;code&gt;langchain-google-genai&lt;/code&gt; 4.3.4, &lt;code&gt;langchain-core&lt;/code&gt; 1.6.0,&lt;br&gt;
&lt;code&gt;sentry-sdk&lt;/code&gt; 2.68.0, &lt;code&gt;gemini-2.5-flash&lt;/code&gt;, Python 3.13.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>bugsmash</title>
      <dc:creator>Hassan Imam</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:15:04 +0000</pubDate>
      <link>https://dev.to/hassan_shah_733ea1eb37c88/bugsmash-1cag</link>
      <guid>https://dev.to/hassan_shah_733ea1eb37c88/bugsmash-1cag</guid>
      <description></description>
    </item>
    <item>
      <title>AccessLens — A persistent on-device visual interpreter for the blind</title>
      <dc:creator>Hassan Imam</dc:creator>
      <pubDate>Sun, 24 May 2026 06:46:43 +0000</pubDate>
      <link>https://dev.to/hassan_shah_733ea1eb37c88/accesslens-a-persistent-on-device-visual-interpreter-for-the-blind-8h6</link>
      <guid>https://dev.to/hassan_shah_733ea1eb37c88/accesslens-a-persistent-on-device-visual-interpreter-for-the-blind-8h6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AccessLens&lt;/strong&gt; is an Android app that turns a Pixel 8 worn on a lanyard into a persistent visual interpreter for blind and low-vision users. Rear camera forward, bone-conduction headphones in, the phone describes the world — and &lt;em&gt;remembers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The problem with existing visual-assist apps (Be My Eyes, Seeing AI, Envision) is that they are screen-bound, stateless, and cloud-bound. A blind person navigates by sound; an app that needs you to hold up a phone, tap a screen, and wait on a datacenter interrupts that signal stream. AccessLens is different on three axes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Worn, not held:&lt;/strong&gt; Two physical buttons drive everything. Volume Up → read text in front of me, verbatim. Volume Down → describe this room with memory from earlier today and recent days. A gyroscope-based &lt;code&gt;SettleTrigger&lt;/code&gt; also fires a description &lt;em&gt;automatically&lt;/em&gt; when the user stops walking.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Persistent memory across days/weeks:&lt;/strong&gt; Every gesture writes a &lt;code&gt;SessionEvent&lt;/code&gt; to a SQLCipher database. A nightly Gemma 4 worker compresses each day into a &lt;code&gt;DailySummary&lt;/code&gt;; Sundays roll into a &lt;code&gt;WeeklyMemory&lt;/code&gt;. LONG-press prompts splice that history into the Gemma call, so the model has a world model of &lt;em&gt;this specific apartment, this specific day&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;100% on-device:&lt;/strong&gt; No image, audio, embedding, or location leaves the phone. SQLCipher + Android KeyStore (AES-256-GCM wrapping a &lt;code&gt;SecureRandom&lt;/code&gt; DB key) protect everything at rest. A &lt;code&gt;SelfTest&lt;/code&gt; on first launch opens a probe DB with the wrong key and asserts the read fails before the app reports encryption healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Face recognition uses MediaPipe FaceLandmarker to produce a 192-dim L2-normalized landmark vector per enrolled person. At identify time, cosine-similar matches inject &lt;strong&gt;only the names&lt;/strong&gt; into the Gemma prompt — Gemma never sees a face crop or an embedding, code-review-verified.&lt;/p&gt;

&lt;p&gt;Three gestures, three target latencies (Pixel 8, Tensor G3): SINGLE ≤14 s end-to-end, DOUBLE scales with text length, LONG adds memory retrieval. Voice fillers ("I'm looking…", "Still looking…") cover the prefill gap so the user hears acoustic progress, not dead air. Everything runs with airplane mode on after the model is pushed once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4ZlaVqXlAc4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/hassaninnovate" rel="noopener noreferrer"&gt;
        hassaninnovate
      &lt;/a&gt; / &lt;a href="https://github.com/hassaninnovate/AccessLens" rel="noopener noreferrer"&gt;
        AccessLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A blind person's lanyard, powered by Gemma 4 E2B on a Pixel 8. 100% on-device visual assistant with persistent memory and face recognition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AccessLens&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;An always-on, on-device visual interpreter for blind and low-vision users — built for the DEV.to "Build with Gemma 4" challenge.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/hassaninnovate/AccessLens/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5b60841bea9e11d9d0b0950d690c9bc554e06385634056a7d5d62a15d1a4eabe/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4170616368655f322e302d626c75652e737667" alt="License: Apache 2.0"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/89ea7be8eadb64987fcd6f39c332b8850d3f1b411750f72dcfc6b907ec73aeac/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d416e64726f696425323031332532422d677265656e2e737667" alt="Platform"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aad418e159887ad6a1a78f63f4092bfe5749f2d8ebcefa4e3187d02db161396f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d6f64656c2d47656d6d615f345f4532422d6f72616e67652e737667" alt="Model"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#privacy-invariants" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0a8d8b5a8c05c0d263eae2efe64ce37bb841d10d59e9ec1d73cf64b8dce0ac4a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696e666572656e63652d3130302532355f6f6e2d2d6465766963652d627269676874677265656e2e737667" alt="Privacy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Pitch&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A phone worn on a lanyard becomes the user's "eyes." The rear camera is always on; the gyroscope watches for motion. &lt;strong&gt;When the user stops walking, AccessLens describes what's in front of them.&lt;/strong&gt; When a friend whose face has been enrolled walks into frame, the phone says their name. When the user wants to read what's in front of them, they press &lt;em&gt;Volume Up&lt;/em&gt;; for a richer description of the room, &lt;em&gt;Volume Down&lt;/em&gt;. Bluetooth bone-conduction headphones carry the audio — the user's ears stay free for the world.&lt;/p&gt;
&lt;p&gt;What separates AccessLens from existing apps like Be My Eyes, Seeing AI, and Envision is &lt;strong&gt;persistent on-device memory + 100% on-device inference&lt;/strong&gt;. Existing tools are stateless and cloud-bound. AccessLens runs Gemma 4 E2B locally via LiteRT-LM, encrypts…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/hassaninnovate/AccessLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Apache 2.0. The repo includes the full Kotlin/Compose source, the encryption self-test, the nightly compression WorkManager job, and a README documenting which file enforces each of the six privacy invariants.&lt;/p&gt;

&lt;p&gt;Reference implementation that taught me the LiteRT-LM API: &lt;a href="https://github.com/google-ai-edge/gallery" rel="noopener noreferrer"&gt;google-ai-edge/gallery&lt;/a&gt; — adapted patterns are cited inline in &lt;code&gt;inference/LiteRtLmRuntime.kt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model Chosen:&lt;/strong&gt; &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;litert-community/gemma-4-E2B-it-litert-lm&lt;/code&gt;, ~2.59 GB int4), loaded once at service start via LiteRT-LM 0.12.0 with &lt;code&gt;Backend.GPU()&lt;/code&gt; for the vision adapter. &lt;/p&gt;

&lt;p&gt;This model was the perfect fit for AccessLens for three core reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal in one model, on-device:&lt;/strong&gt; Image input goes in as &lt;code&gt;Content.ImageBytes&lt;/code&gt;, text as &lt;code&gt;Content.Text&lt;/code&gt;, in that order (per the Gallery's "for accurate last token" comment), all through one &lt;code&gt;Engine.generate&lt;/code&gt; call. No separate vision encoder + decoder to stitch, no second model to keep resident. That fits the latency budget &lt;em&gt;and&lt;/em&gt; the memory budget on Pixel-class 8 GB RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E2B is the smallest competent multimodal Gemma 4:&lt;/strong&gt; It fits in RAM alongside MediaPipe FaceLandmarker, a CameraX pipeline, and the Compose UI without OOM-ing on a Pixel 8. I prototyped against E4B (the brief's "quality path") and measured the latency lift on one-sentence scene descriptions — not worth doubling the prefill cost for a use case where the user is waiting in real time, lanyard-mounted, with no screen feedback. The architecture is &lt;em&gt;parametric&lt;/em&gt; on the model path (&lt;code&gt;InferenceRuntime.load(modelPath, Modality)&lt;/code&gt;), so a future LONG-press branch could swap to E4B in one line. I documented the tradeoff in the README and shipped E2B for all three gestures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma is the only practical way to do nightly memory compression on-device:&lt;/strong&gt; The 03:00 &lt;code&gt;CompressionWorker&lt;/code&gt; calls Gemma in JSON mode to compress the day's &lt;code&gt;SessionEvent&lt;/code&gt; rows into a single &lt;code&gt;DailySummary&lt;/code&gt;, and on Sundays into a &lt;code&gt;WeeklyMemory&lt;/code&gt;. That's a real LLM task — extracting persistent facts, deduplicating recurring observations, distinguishing "the blue mug is mine" from "I saw a blue mug today" — and it has to happen without a network. E2B handles it in under a minute per day on Tensor G3 while the phone is on the charger.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Production fixes discovered during implementation:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  The LiteRT-LM Android artifact must be &lt;strong&gt;0.12.0 or later&lt;/strong&gt; — 0.11.0 fails vision init inside &lt;code&gt;vision_litert_compiled_model_executor.cc:273&lt;/code&gt; on Tensor G3.&lt;/li&gt;
&lt;li&gt;  AndroidManifest needs &lt;code&gt;&amp;lt;uses-native-library&amp;gt;&lt;/code&gt; declarations for &lt;code&gt;libOpenCL.so&lt;/code&gt;, &lt;code&gt;libOpenCL-car.so&lt;/code&gt;, &lt;code&gt;libOpenCL-pixel.so&lt;/code&gt; (all &lt;code&gt;android:required="false"&lt;/code&gt;). Without them, Android 12+ silently denies GPU OpenCL access and the vision backend fails to initialize. Documented at &lt;a href="https://ai.google.dev/edge/litert-lm/android" rel="noopener noreferrer"&gt;ai.google.dev/edge/litert-lm/android&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The thing I'm proudest of:&lt;/strong&gt; when you uninstall AccessLens, the KeyStore wrapping key is destroyed with it. The encrypted DB on disk becomes cryptographically unrecoverable. The user can throw the phone away and their memories — kitchen layout, friends' faces, places they've been — go with it. That's what on-device privacy is supposed to mean, and Gemma 4 + LiteRT-LM made it possible without compromising the assistant on quality.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>AccessLens — a blind person's lanyard, powered by Gemma 4 on-device</title>
      <dc:creator>Hassan Imam</dc:creator>
      <pubDate>Sat, 23 May 2026 17:27:35 +0000</pubDate>
      <link>https://dev.to/hassan_shah_733ea1eb37c88/accesslens-a-blind-persons-lanyard-powered-by-gemma-4-on-device-3l8b</link>
      <guid>https://dev.to/hassan_shah_733ea1eb37c88/accesslens-a-blind-persons-lanyard-powered-by-gemma-4-on-device-3l8b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AccessLens&lt;/strong&gt; is an Android app that turns a Pixel 8 worn on a lanyard into a persistent visual interpreter for blind and low-vision users. Rear camera forward, bone-conduction headphones in, the phone describes the world — and &lt;em&gt;remembers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The problem with existing visual-assist apps (Be My Eyes, Seeing AI, Envision) is that they are screen-bound, stateless, and cloud-bound. A blind person navigates by sound; an app that needs you to hold up a phone, tap a screen, and wait on a datacenter interrupts that signal stream. AccessLens is different on three axes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worn, not held.&lt;/strong&gt; Two physical buttons drive everything. Volume Up → read text in front of me, verbatim. Volume Down → describe this room with memory from earlier today and recent days. A gyroscope-based &lt;code&gt;SettleTrigger&lt;/code&gt; also fires a description &lt;em&gt;automatically&lt;/em&gt; when the user stops walking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent memory across days/weeks.&lt;/strong&gt; Every gesture writes a &lt;code&gt;SessionEvent&lt;/code&gt; to a SQLCipher database. A nightly Gemma 4 worker compresses each day into a &lt;code&gt;DailySummary&lt;/code&gt;; Sundays roll into a &lt;code&gt;WeeklyMemory&lt;/code&gt;. LONG-press prompts splice that history into the Gemma call, so the model has a world model of &lt;em&gt;this specific apartment, this specific day&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100% on-device.&lt;/strong&gt; No image, audio, embedding, or location leaves the phone. SQLCipher + Android KeyStore (AES-256-GCM wrapping a &lt;code&gt;SecureRandom&lt;/code&gt; DB key) protect everything at rest. A &lt;code&gt;SelfTest&lt;/code&gt; on first launch opens a probe DB with the wrong key and asserts the read fails before the app reports encryption healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Face recognition uses MediaPipe FaceLandmarker to produce a 192-dim L2-normalized landmark vector per enrolled person. At identify time, cosine-similar matches inject &lt;strong&gt;only the names&lt;/strong&gt; into the Gemma prompt — Gemma never sees a face crop or an embedding, code-review-verified.&lt;/p&gt;

&lt;p&gt;Three gestures, three target latencies (Pixel 8, Tensor G3): SINGLE ≤14 s end-to-end, DOUBLE scales with text length, LONG adds memory retrieval. Voice fillers ("I'm looking…", "Still looking…") cover the prefill gap so the user hears acoustic progress, not dead air. Everything runs with airplane mode on after the model is pushed once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4ZlaVqXlAc4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/hassaninnovate" rel="noopener noreferrer"&gt;
        hassaninnovate
      &lt;/a&gt; / &lt;a href="https://github.com/hassaninnovate/AccessLens" rel="noopener noreferrer"&gt;
        AccessLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A blind person's lanyard, powered by Gemma 4 E2B on a Pixel 8. 100% on-device visual assistant with persistent memory and face recognition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;AccessLens&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;An always-on, on-device visual interpreter for blind and low-vision users — built for the DEV.to "Build with Gemma 4" challenge.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/hassaninnovate/AccessLens/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/5b60841bea9e11d9d0b0950d690c9bc554e06385634056a7d5d62a15d1a4eabe/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4170616368655f322e302d626c75652e737667" alt="License: Apache 2.0"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/89ea7be8eadb64987fcd6f39c332b8850d3f1b411750f72dcfc6b907ec73aeac/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f706c6174666f726d2d416e64726f696425323031332532422d677265656e2e737667" alt="Platform"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/aad418e159887ad6a1a78f63f4092bfe5749f2d8ebcefa4e3187d02db161396f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d6f64656c2d47656d6d615f345f4532422d6f72616e67652e737667" alt="Model"&gt;&lt;/a&gt;
&lt;a href="https://github.com/hassaninnovate/AccessLens#privacy-invariants" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/0a8d8b5a8c05c0d263eae2efe64ce37bb841d10d59e9ec1d73cf64b8dce0ac4a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696e666572656e63652d3130302532355f6f6e2d2d6465766963652d627269676874677265656e2e737667" alt="Privacy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Pitch&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A phone worn on a lanyard becomes the user's "eyes." The rear camera is always on; the gyroscope watches for motion. &lt;strong&gt;When the user stops walking, AccessLens describes what's in front of them.&lt;/strong&gt; When a friend whose face has been enrolled walks into frame, the phone says their name. When the user wants to read what's in front of them, they press &lt;em&gt;Volume Up&lt;/em&gt;; for a richer description of the room, &lt;em&gt;Volume Down&lt;/em&gt;. Bluetooth bone-conduction headphones carry the audio — the user's ears stay free for the world.&lt;/p&gt;
&lt;p&gt;What separates AccessLens from existing apps like Be My Eyes, Seeing AI, and Envision is &lt;strong&gt;persistent on-device memory + 100% on-device inference&lt;/strong&gt;. Existing tools are stateless and cloud-bound. AccessLens runs Gemma 4 E2B locally via LiteRT-LM, encrypts…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/hassaninnovate/AccessLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Apache 2.0. The repo includes the full Kotlin/Compose source, the encryption self-test, the nightly compression WorkManager job, and a README documenting which file enforces each of the six privacy invariants.&lt;/p&gt;

&lt;p&gt;Reference implementation that taught me the LiteRT-LM API: &lt;a href="https://github.com/google-ai-edge/gallery" rel="noopener noreferrer"&gt;google-ai-edge/gallery&lt;/a&gt; — adapted patterns are cited inline in &lt;code&gt;inference/LiteRtLmRuntime.kt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Model: Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;litert-community/gemma-4-E2B-it-litert-lm&lt;/code&gt;, ~2.59 GB int4), loaded once at service start via LiteRT-LM 0.12.0 with &lt;code&gt;Backend.GPU()&lt;/code&gt; for the vision adapter. Three reasons E2B was the right fit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multimodal in one model, on-device.&lt;/strong&gt; Image input goes in as &lt;code&gt;Content.ImageBytes&lt;/code&gt;, text as &lt;code&gt;Content.Text&lt;/code&gt;, in that order (per the Gallery's "for accurate last token" comment), all through one &lt;code&gt;Engine.generate&lt;/code&gt; call. No separate vision encoder + decoder to stitch, no second model to keep resident. That fits the latency budget &lt;em&gt;and&lt;/em&gt; the memory budget on Pixel-class 8 GB RAM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;E2B is the smallest competent multimodal Gemma 4.&lt;/strong&gt; It fits in RAM alongside MediaPipe FaceLandmarker, a CameraX pipeline, and the Compose UI without OOM-ing on a Pixel 8. I prototyped against E4B (the brief's "quality path") and measured the latency lift on one-sentence scene descriptions — not worth doubling the prefill cost for a use case where the user is waiting in real time, lanyard-mounted, with no screen feedback. The architecture is &lt;em&gt;parametric&lt;/em&gt; on the model path (&lt;code&gt;InferenceRuntime.load(modelPath, Modality)&lt;/code&gt;), so a future LONG-press branch could swap to E4B in one line. I documented the tradeoff in the README and shipped E2B for all three gestures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gemma is the only practical way to do nightly memory compression on-device.&lt;/strong&gt; The 03:00 &lt;code&gt;CompressionWorker&lt;/code&gt; calls Gemma in JSON mode to compress the day's &lt;code&gt;SessionEvent&lt;/code&gt; rows into a single &lt;code&gt;DailySummary&lt;/code&gt;, and on Sundays into a &lt;code&gt;WeeklyMemory&lt;/code&gt;. That's a real LLM task — extracting persistent facts, deduplicating recurring observations, distinguishing "the blue mug is mine" from "I saw a blue mug today" — and it has to happen without a network. E2B handles it in under a minute per day on Tensor G3 while the phone is on the charger.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Two production fixes the brief didn't cover, in case they help someone else:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The LiteRT-LM Android artifact must be &lt;strong&gt;0.12.0 or later&lt;/strong&gt; — 0.11.0 fails vision init inside &lt;code&gt;vision_litert_compiled_model_executor.cc:273&lt;/code&gt; on Tensor G3.&lt;/li&gt;
&lt;li&gt;AndroidManifest needs &lt;code&gt;&amp;lt;uses-native-library&amp;gt;&lt;/code&gt; declarations for &lt;code&gt;libOpenCL.so&lt;/code&gt;, &lt;code&gt;libOpenCL-car.so&lt;/code&gt;, &lt;code&gt;libOpenCL-pixel.so&lt;/code&gt; (all &lt;code&gt;android:required="false"&lt;/code&gt;). Without them, Android 12+ silently denies GPU OpenCL access and the vision backend fails to initialize. Documented at &lt;a href="https://ai.google.dev/edge/litert-lm/android" rel="noopener noreferrer"&gt;ai.google.dev/edge/litert-lm/android&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The thing I'm proudest of:&lt;/strong&gt; when you uninstall AccessLens, the KeyStore wrapping key is destroyed with it. The encrypted DB on disk becomes cryptographically unrecoverable. The user can throw the phone away and their memories — kitchen layout, friends' faces, places they've been — go with it. That's what on-device privacy is supposed to mean, and Gemma 4 + LiteRT-LM made it possible without compromising the assistant on quality.&lt;/p&gt;

</description>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>android</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
