<?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: Ahmed Nafies</title>
    <description>The latest articles on DEV Community by Ahmed Nafies (@ahmed_nafies_3a55c907115c).</description>
    <link>https://dev.to/ahmed_nafies_3a55c907115c</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%2F4055741%2F3f230fa6-829b-417e-9dd7-bb23785258a5.png</url>
      <title>DEV Community: Ahmed Nafies</title>
      <link>https://dev.to/ahmed_nafies_3a55c907115c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmed_nafies_3a55c907115c"/>
    <language>en</language>
    <item>
      <title>I built an agentic coding exam for Hy4 preview, then made the model sit it</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Sat, 29 Aug 2026 11:14:55 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/i-built-an-agentic-coding-exam-for-hy4-preview-then-made-the-model-sit-it-393</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/i-built-an-agentic-coding-exam-for-hy4-preview-then-made-the-model-sit-it-393</guid>
      <description>&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%2Fvtw3d3m93jb8bhuryzno.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%2Fvtw3d3m93jb8bhuryzno.png" alt="Hy4 Preview" width="800" height="602"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tencent open-sourced Hy4 preview on 28 August 2026. 770B parameters, 49B active per token, a 1M context, Apache 2.0 weights, and a model card that admits two unflattering things about itself: it "spends longer than necessary reasoning through complex tasks" and it has "a tendency to over-verify its own work."&lt;/p&gt;

&lt;p&gt;The benchmarks look good (GPQA Diamond 92.3, SWE-Bench Multilingual 82.9, HLE with tools 55.4). Tencent's own blind eval puts it 0.07 ahead of GLM 5.3 and 0.05 ahead of Kimi K3. On a 3-point scale, margins that small are noise, and none of it tells me whether the model is any good in my editor. So I built a smaller test: a repo with a missing feature, the rules hidden in a policy document, and 26 tests that will not pass until the feature exists.&lt;/p&gt;

&lt;p&gt;One disclosure before the results. I wrote the test and I also took it, running as &lt;code&gt;opencode-go/hy4-preview&lt;/code&gt; inside &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;opencode&lt;/a&gt;. Read this as a field report from one afternoon, not a study.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test bed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;orderflow&lt;/code&gt; is a toy event-sourced order pipeline. Orders get placed, taxed, captured and fulfilled. It cannot give money back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/orderflow/
  money.py      # integer cents, banker's rounding, largest-remainder allocate()
  events.py     # frozen dataclasses + a name -&amp;gt; class registry + (de)serialization
  state.py      # Order aggregate, TaxTable, OrderBook.apply(event)
  pipeline.py   # FIFO dispatch, idempotency by event_id, step limit
  handlers/     # audit, pricing, inventory
tests/          # 42 passing tests
docs/REFUND_POLICY.md
SPEC.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task is to implement refunds, and to do it by extending the architecture rather than bolting a function onto the side: new events, new aggregate state, a new handler, all wired into the registries that already exist.&lt;/p&gt;

&lt;p&gt;Three things make it harder than it looks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules live in a document, not in the prompt.&lt;/strong&gt; &lt;code&gt;SPEC.md&lt;/code&gt; is a short brief. It says, in effect: go read &lt;code&gt;docs/REFUND_POLICY.md&lt;/code&gt;, go read the tests, follow the conventions you find. The policy document is where the validation order, the clamping rule, the proportional split and the idempotency requirement actually live. Skim the brief and start typing and you get something that looks right and is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The registry is a forcing function.&lt;/strong&gt; Events register themselves by name, and one generic test walks the entire registry:&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="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EVENT_REGISTRY&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;test_every_registered_event_survives_serialization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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="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="n"&gt;cls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EVENT_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_sample_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# builds an instance from type hints
&lt;/span&gt;    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deserialize_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;serialize_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You cannot add an event and forget to register it. You cannot add a field whose type the serializer cannot round-trip. That one test ended up being worth more than the 20-odd tests that spell out refund behaviour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The money is the point.&lt;/strong&gt; Refunds split across line items, and the split has to be exact. &lt;code&gt;Money.allocate()&lt;/code&gt; uses the largest remainder method, the net portion rounds down, and the tax portion absorbs the difference, so &lt;code&gt;net + tax == share&lt;/code&gt; on every line and the shares add back to the refunded amount. Refund a 49.48 order as three partials plus a final sweep and you still land on 4948 cents, not 4947.&lt;/p&gt;

&lt;h2&gt;
  
  
  The run
&lt;/h2&gt;

&lt;p&gt;Baseline, before any solution code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;42 passed                      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;everything except the contract
&lt;span class="gp"&gt;ERROR tests/test_refunds.py    #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ImportError: cannot import name &lt;span class="s1"&gt;'RefundFailed'&lt;/span&gt;
&lt;span class="go"&gt;ruff: clean
mypy: 16 errors, all in test_refunds.py
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the implementation: three events and two value objects in &lt;code&gt;events.py&lt;/code&gt;, per-SKU refund tracking plus a &lt;code&gt;RefundIssued&lt;/code&gt; branch in &lt;code&gt;OrderBook.apply&lt;/code&gt;, and an 81-line handler in &lt;code&gt;handlers/refunds.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eight failures on the first run. One on the second. Green on the third.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;74 passed
ruff check .   All checks passed!
mypy src tests Success: no issues found in 18 source files
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scale: about 81 lines of handler, 40 of events, 35 of aggregate state, and 5 lines of wiring in &lt;code&gt;handlers/__init__.py&lt;/code&gt; and &lt;code&gt;AUDITED_EVENTS&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two bugs the fixed tests did not catch
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;X | None&lt;/code&gt; is not &lt;code&gt;Optional[X]&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The round-trip test failed on &lt;code&gt;refund.requested&lt;/code&gt;, whose scope field 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="n"&gt;line_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&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="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;typing.get_origin()&lt;/code&gt; returns &lt;code&gt;types.UnionType&lt;/code&gt; for the PEP 604 form and &lt;code&gt;typing.Union&lt;/code&gt; for &lt;code&gt;Optional[X]&lt;/code&gt;. The serializer only handled &lt;code&gt;typing.Union&lt;/code&gt;, so the union branch was skipped, &lt;code&gt;None&lt;/code&gt; fell through to the tuple branch, and decoding died with &lt;code&gt;TypeError: 'NoneType' object is not iterable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is a production deserialization crash sitting one annotation away, and every hand-written test stepped over it, because every hand-written example passed a real tuple.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The one the fuzzer found
&lt;/h3&gt;

&lt;p&gt;With the suite green I wrote a throwaway randomized harness: random orders, random SKU subsets, random refund sequences, checking invariants after every step. Per-line sums, per-SKU ceilings, refunded no greater than captured, the status flip, idempotency on replay, and a serialization round trip on every emitted event.&lt;/p&gt;

&lt;p&gt;It failed on the first run, here:&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;_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__type__&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_decode_tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;line_items&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, the encoded payload carries a plain &lt;code&gt;null&lt;/code&gt;. There is no tagged-dict branch to take, so &lt;code&gt;None&lt;/code&gt; reaches the union branch, picks the first non-&lt;code&gt;None&lt;/code&gt; argument, and tries to iterate it. One line fixed it:&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;value&lt;/span&gt; &lt;span class="ow"&gt;is&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;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three thousand randomized scenarios now hold every invariant, and a seeded 200-case version lives in the suite as &lt;code&gt;tests/test_refunds_property.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example-based tests encode the cases you already thought of. For money, allocation and serialization, those are the easy ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disagreement
&lt;/h2&gt;

&lt;p&gt;One test failed with a reason code I did not expect:&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="nf"&gt;issued&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;req-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;req-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;RefundFailureReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXCEEDS_REFUNDABLE&lt;/span&gt;   &lt;span class="c1"&gt;# got STATUS_NOT_REFUNDABLE
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both answers are defensible. Refund an order in full and two things are true at once: the order is now in state &lt;code&gt;REFUNDED&lt;/code&gt;, which is not in &lt;code&gt;REFUNDABLE_STATUSES&lt;/code&gt;, and there is nothing left to refund.&lt;/p&gt;

&lt;p&gt;The policy document settles it, in the model's favour. It defines a validation order and says to stop at the first failure, with &lt;code&gt;status_not_refundable&lt;/code&gt; at position 5 and &lt;code&gt;exceeds_refundable&lt;/code&gt; at position 7. A fully-refunded order trips rule 5. My test was wrong, the document was right, so I changed the test and added a separate case that exercises &lt;code&gt;EXCEEDS_REFUNDABLE&lt;/code&gt; properly: refund SKU A completely, then ask for more of A while B still has money.&lt;/p&gt;

&lt;p&gt;The other direction is worth thinking about, because it is the one a compliant model would have taken. To make my test pass it could have reordered the validation rules or quietly widened &lt;code&gt;REFUNDABLE_STATUSES&lt;/code&gt;. Either change passes review and produces a refund bug six months later. It pushed back instead and said which document it was following.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The over-verification is real.&lt;/strong&gt; I wrote a 3,000-scenario property harness for a toy repo with no users, which is silly by any reasonable accounting. It also caught a crash that 74 green tests missed. The trait the model card lists as a weakness is the trait that caught the bug. I will take that trade on anything touching money. I would not want to pay for it on a ten-minute task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reads before it writes, and it treats the tests as the spec.&lt;/strong&gt; The first move was a grep over the test file to extract the exact API surface the contract expected (&lt;code&gt;refunded_amount&lt;/code&gt;, &lt;code&gt;refundable_remaining&lt;/code&gt;, &lt;code&gt;refunded_net_by_sku&lt;/code&gt;) instead of inventing names and iterating until they matched. Small thing, and it is most of the difference between useful in a real repo and impressive in a demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It used the allocation rule instead of approximating it.&lt;/strong&gt; The brief says to use &lt;code&gt;Money.allocate&lt;/code&gt; and not to hand-roll proportions, and the implementation does exactly that, including skipping zero-value lines and clamping each line to what remains for that SKU. No drift anywhere in the randomized runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its own test code was the weak part.&lt;/strong&gt; Seven of the eight initial failures were bugs in the contract I had written: tests that asked for a refund without placing an order first, and so got &lt;code&gt;ORDER_NOT_FOUND&lt;/code&gt;. When a model writes both the exam and the answers, a broken exam is still broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing this
&lt;/h2&gt;

&lt;p&gt;The repo takes about ten minutes to read and the interesting knob is &lt;code&gt;docs/REFUND_POLICY.md&lt;/code&gt;. Change the policy and you get a different exam over the same codebase: refunds that restock inventory, tax that is not refundable, a restocking fee that has to be allocated across lines, a different validation order. The money invariants stay put.&lt;/p&gt;

&lt;p&gt;To point another model at it, hand over &lt;code&gt;SPEC.md&lt;/code&gt; as the only prompt, forbid edits under &lt;code&gt;tests/&lt;/code&gt;, and require &lt;code&gt;pytest&lt;/code&gt;, &lt;code&gt;ruff check .&lt;/code&gt; and &lt;code&gt;mypy&lt;/code&gt; to be clean. The collection error at the start is deliberate. The model has to infer an API surface from tests that import symbols which do not exist yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, does it impress me?
&lt;/h2&gt;

&lt;p&gt;On the narrow question I actually care about, yes. Given a repo where the rules sit in a document and the arithmetic has to come out exact, it read everything first, produced a correct implementation in one pass, refused to fudge a test it could have satisfied by quietly breaking the spec, and then found two bugs I had shipped into my own suite. That is the behaviour I want from an agent working in a codebase I have to maintain.&lt;/p&gt;

&lt;p&gt;On the question the benchmarks pretend to answer, no, and the numbers do not support it either. A 0.07 margin across 203 tasks rated by 163 people is a tie. One self-administered task on a codebase I designed is not evidence of a step change. What I have is a single data point saying the model behaves well under the conditions where I would actually use it, which is worth more to me than the benchmark table and much less than a real evaluation.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I'd use it, and when I wouldn't
&lt;/h2&gt;

&lt;p&gt;Use it for long-horizon agentic work that keeps resending a large system prompt and a growing tool history. The economics here are better than the headline prices suggest: at launch the listings had it at $0.834 per million input, $2.501 per million output, and $0.042 per million for cached input, which is a 20x discount off the input rate. Agent loops are mostly resends, so the cached rate is the number that decides your bill. It also undercuts GLM 5.3 by roughly 40% on output and Kimi K3 by about 6x.&lt;/p&gt;

&lt;p&gt;Use it when you need genuine long context. 1M tokens is more than GLM 5.3 offers, and it is the main thing here that no cheaper open model matches.&lt;/p&gt;

&lt;p&gt;Use it if Apache 2.0 with no field-of-use clause matters to you, which for most companies it does.&lt;/p&gt;

&lt;p&gt;Do not use it if you are optimizing on price alone and do not need the context. DeepSeek V4 Pro costs roughly half as much on input and a third as much on output. That is still the floor and Hy4 does not go under it.&lt;/p&gt;

&lt;p&gt;Do not use it for anything latency-sensitive. OpenRouter listed 43 tok/s best throughput at launch and one reviewer measured 36. That is slow when a human is waiting on the other end of an interactive loop.&lt;/p&gt;

&lt;p&gt;Think twice before pinning production traffic to it at all. The card calls this an early version shipping with known issues, and the preview label means the checkpoint will be replaced.&lt;/p&gt;

&lt;p&gt;One habit I would keep whichever way you go: let it write the implementation, and review the tests it writes yourself. Here the code was right and my tests were wrong, seven times over.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>I'm using GPT Sol and Claude Opus for free — pi-coding-agent + OmniRoute</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:11:56 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/im-using-gpt-sol-and-claude-opus-for-free-pi-coding-agent-omniroute-36m8</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/im-using-gpt-sol-and-claude-opus-for-free-pi-coding-agent-omniroute-36m8</guid>
      <description>&lt;p&gt;I'm using GPT Sol and Claude Opus for free. No credit card, no per-token bill, no "you've hit your monthly limit" email.&lt;/p&gt;

&lt;p&gt;Two pieces of software make it happen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi&lt;/strong&gt; — the coding agent that reads, writes, and runs commands in my terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OmniRoute&lt;/strong&gt; — a local gateway that sits in front of hundreds of AI providers, dozens of them free, and routes every request to the cheapest one that works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the exact setup, from install to first prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "free" means here
&lt;/h2&gt;

&lt;p&gt;OmniRoute ships with free-tier providers pre-wired into the &lt;code&gt;auto&lt;/code&gt; model. You don't paste an API key — a fresh install just answers. But &lt;code&gt;auto&lt;/code&gt; also reaches frontier-class models through the free routes, so I can pick them by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aug/gpt5.6-sol       # GPT Sol
aug/opus4.8          # Claude Opus
auto/best-coding     # best free coding model
auto/best-reasoning  # best free reasoning model
auto                 # let OmniRoute pick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pi talks to one endpoint (&lt;code&gt;http://localhost:20128/v1&lt;/code&gt;). OmniRoute handles which provider actually serves the request, and falls back automatically if one goes down or hits a rate limit.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Install both
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; omniroute
omniroute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway boots on port &lt;code&gt;20128&lt;/code&gt;; the dashboard is at &lt;code&gt;http://localhost:20128&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @earendil-works/pi-coding-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Quick note so you install the right thing: this is the npm &lt;code&gt;pi&lt;/code&gt; (&lt;code&gt;@earendil-works/pi-coding-agent&lt;/code&gt;), a coding agent with read / bash / edit / write tools. It's unrelated to the Rust project also called &lt;code&gt;pi-coding-agent&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Add OmniRoute as a Pi provider
&lt;/h2&gt;

&lt;p&gt;Pi reads its providers from &lt;code&gt;~/.pi/agent/models.json&lt;/code&gt;. Add an &lt;code&gt;omniroute&lt;/code&gt; entry:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"providers"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"omniroute"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai-completions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:20128/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"models"&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"_launch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto/best-coding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto/best-reasoning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto/best-fast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aug/gpt5.6-sol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aug/opus4.8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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="p"&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api: "openai-completions"&lt;/code&gt; — OmniRoute speaks the OpenAI protocol.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;baseUrl&lt;/code&gt; — the local gateway. Keep the &lt;code&gt;/v1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apiKey&lt;/code&gt; — empty is fine: OmniRoute runs keyless locally. If you've created a key on the dashboard's Endpoints page, put it here instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already have providers in that file, just add the &lt;code&gt;omniroute&lt;/code&gt; key next to them — don't overwrite the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Make it the default
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;~/.pi/agent/settings.json&lt;/code&gt;:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaultProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"omniroute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaultModel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;pi&lt;/code&gt; uses OmniRoute out of the box, no flags needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pi &lt;span class="s2"&gt;"fix the failing test in src"&lt;/span&gt;      &lt;span class="c"&gt;# interactive, routes through OmniRoute&lt;/span&gt;
pi &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"summarize this file"&lt;/span&gt;           &lt;span class="c"&gt;# non-interactive, print and exit&lt;/span&gt;
pi &lt;span class="nt"&gt;--model&lt;/span&gt; aug/gpt5.6-sol             &lt;span class="c"&gt;# GPT Sol specifically&lt;/span&gt;
pi &lt;span class="nt"&gt;--model&lt;/span&gt; aug/opus4.8                &lt;span class="c"&gt;# Claude Opus specifically&lt;/span&gt;
pi &lt;span class="nt"&gt;--model&lt;/span&gt; auto/best-reasoning        &lt;span class="c"&gt;# best free reasoning model&lt;/span&gt;
pi &lt;span class="nt"&gt;-c&lt;/span&gt;                                 &lt;span class="c"&gt;# continue the last session&lt;/span&gt;
pi &lt;span class="nt"&gt;--list-models&lt;/span&gt;                      &lt;span class="c"&gt;# see every available model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switching models is one flag — no reconfiguring keys, no new accounts. If you ever want your local Ollama model back, &lt;code&gt;pi --provider ollama&lt;/code&gt; does it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two gotchas that cost me time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The dashboard writes the wrong file for this Pi.&lt;/strong&gt; OmniRoute's built-in "Pi" integration (dashboard → CLI tools → Pi) writes &lt;code&gt;~/.pi/config.json&lt;/code&gt;. This Pi reads &lt;code&gt;~/.pi/agent/models.json&lt;/code&gt;. Edit the file by hand as above and you're done — the dashboard button won't do it for this version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Bare OpenAI model names fail.&lt;/strong&gt; Requesting &lt;code&gt;gpt-5.6-luna&lt;/code&gt; (no prefix) returns &lt;code&gt;No active credentials for provider: openai&lt;/code&gt;. The free routes are namespaced — use &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;auto/*&lt;/code&gt;, or &lt;code&gt;aug/*&lt;/code&gt; IDs like &lt;code&gt;aug/gpt5.6-sol&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this beats juggling provider keys
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0&lt;/strong&gt; — no card, no bill, no key management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One endpoint&lt;/strong&gt; — every tool and model behind &lt;code&gt;http://localhost:20128/v1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-fallback&lt;/strong&gt; — a rate-limited or dead provider swaps out without you noticing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One config&lt;/strong&gt; — switching models in Pi is &lt;code&gt;--model&lt;/code&gt;, not a support ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install OmniRoute, point Pi at it, and go. Ten minutes, and GPT Sol and Claude Opus are just models you can pick.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Testing OmniRoute locally: verify your AI gateway actually works before you trust it</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:11:45 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/testing-omniroute-locally-verify-your-ai-gateway-actually-works-before-you-trust-it-1pde</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/testing-omniroute-locally-verify-your-ai-gateway-actually-works-before-you-trust-it-1pde</guid>
      <description>&lt;p&gt;OmniRoute is a local AI gateway: it puts one OpenAI-compatible endpoint (&lt;code&gt;http://localhost:20128/v1&lt;/code&gt;) in front of hundreds of providers, then routes requests between them with automatic fallback. Install it, point your coding tool at &lt;code&gt;auto&lt;/code&gt;, and it "just works."&lt;/p&gt;

&lt;p&gt;But "it starts" and "it works" are not the same thing. A gateway that answers &lt;code&gt;Hello!&lt;/code&gt; but falls over on streaming, tool calls, or a provider outage is decorative, not functional. This article is a checklist for proving a fresh local install actually does its job before you wire a coding agent into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Install and boot it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; omniroute
omniroute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server boots on port &lt;code&gt;20128&lt;/code&gt;, the dashboard opens at &lt;code&gt;http://localhost:20128&lt;/code&gt;, and the API base URL is &lt;code&gt;http://localhost:20128/v1&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you use pnpm, add the native build flags: &lt;code&gt;pnpm add -g omniroute@latest --allow-build=better-sqlite3 --allow-build=@swc/core&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A fresh install is zero-config: the free providers (OpenCode Free, Felo) are pre-wired into the &lt;code&gt;auto&lt;/code&gt; combo, so a brand-new server answers out of the box with no API key.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Run local diagnostics (&lt;code&gt;omniroute doctor&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;OmniRoute ships a health checker that runs without starting the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omniroute doctor
omniroute doctor &lt;span class="nt"&gt;--json&lt;/span&gt;        &lt;span class="c"&gt;# machine-readable output&lt;/span&gt;
omniroute doctor &lt;span class="nt"&gt;--no-liveness&lt;/span&gt; &lt;span class="c"&gt;# skip live checks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat this as the first gate. If &lt;code&gt;doctor&lt;/code&gt; reports a broken runtime, native module, or DB issue, fix it before touching the network.&lt;/p&gt;

&lt;p&gt;Other useful CLI probes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omniroute status            &lt;span class="c"&gt;# offline dashboard: version, DB, tools, config&lt;/span&gt;
omniroute providers list    &lt;span class="c"&gt;# what's actually connected&lt;/span&gt;
omniroute providers validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Verify the model list
&lt;/h2&gt;

&lt;p&gt;A connected provider should show up in &lt;code&gt;/v1/models&lt;/code&gt;. If you've created an API key on the &lt;strong&gt;Endpoints&lt;/strong&gt; page, query with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:20128/v1/models &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer YOUR_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A non-empty list means the gateway can see providers. But don't stop here — a model list is not a working request.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Smoke-test a real request
&lt;/h2&gt;

&lt;p&gt;The fastest end-to-end proof:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:20128/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"auto","messages":[{"role":"user","content":"Hello!"}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also call a specific free backend directly (e.g. &lt;code&gt;oc/...&lt;/code&gt; for OpenCode Free) to isolate one provider, then graduate to &lt;code&gt;auto&lt;/code&gt; and let the router pick.&lt;/p&gt;

&lt;p&gt;Each response carries an &lt;code&gt;X-OmniRoute-Decision&lt;/code&gt; header naming the strategy, provider, and latency that served it — use it to confirm routing is doing what you expect.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Test streaming
&lt;/h2&gt;

&lt;p&gt;Many tools depend on streaming. Send the same request with &lt;code&gt;"stream": true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-N&lt;/span&gt; http://localhost:20128/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model":"auto","stream":true,"messages":[{"role":"user","content":"Count to ten."}]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;data:&lt;/code&gt; chunks arriving incrementally rather than one blob. A gateway that only works in non-streaming mode will break most agent CLIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Test tool calling
&lt;/h2&gt;

&lt;p&gt;Coding agents live on tool/function calling. Verify it round-trips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:20128/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "auto",
    "messages": [{"role": "user", "content": "What is the weather in Lisbon?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather",
        "parameters": {
          "type": "object",
          "properties": {"city": {"type": "string"}},
          "required": ["city"]
        }
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid reply should include a &lt;code&gt;tool_calls&lt;/code&gt; block with correctly structured arguments, not a plain-text answer pretending to be a tool call.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Test fallback (the whole point)
&lt;/h2&gt;

&lt;p&gt;The reason you run a gateway is resilience. To prove it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;strong&gt;combo&lt;/strong&gt; on the dashboard with two or more providers.&lt;/li&gt;
&lt;li&gt;Watch the &lt;code&gt;X-OmniRoute-Decision&lt;/code&gt; header to confirm the primary provider is serving.&lt;/li&gt;
&lt;li&gt;Intentionally break the primary (remove its key, exhaust its quota, or disable it), then repeat the request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The request should still succeed, now routed to the next provider — silently, with no error surfaced to your client. If it 500s instead, your fallback chain isn't wired the way you think.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Point a real tool at it
&lt;/h2&gt;

&lt;p&gt;Once the API surface checks out, connect an actual agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base URL: http://localhost:20128/v1
API Key:  [copy from the Endpoints page]
Model:    auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or let OmniRoute write the config for you, per tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omniroute setup-codex      &lt;span class="c"&gt;# ~/.codex/&amp;lt;name&amp;gt;.config.toml profiles&lt;/span&gt;
omniroute setup-claude     &lt;span class="c"&gt;# ~/.claude/profiles/&amp;lt;name&amp;gt;/settings.json&lt;/span&gt;
omniroute setup-opencode   &lt;span class="c"&gt;# opencode.json&lt;/span&gt;
omniroute setup-cursor     &lt;span class="c"&gt;# prints Cursor's in-app steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even simpler — launch a CLI through the gateway with no config written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;omniroute run claude   &lt;span class="nt"&gt;--model&lt;/span&gt; auto
omniroute run codex    &lt;span class="nt"&gt;--model&lt;/span&gt; auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then do a real task (edit a file, run a tool) rather than a hello-world. A finished task proves the whole pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Pass condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;omniroute doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No failing checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;curl /v1/models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Providers listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plain chat&lt;/td&gt;
&lt;td&gt;Valid completion + &lt;code&gt;X-OmniRoute-Decision&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Incremental &lt;code&gt;data:&lt;/code&gt; chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calling&lt;/td&gt;
&lt;td&gt;Structured &lt;code&gt;tool_calls&lt;/code&gt; in the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;Request survives a dead primary provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real agent task&lt;/td&gt;
&lt;td&gt;A coding CLI completes an actual edit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  When a check fails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;doctor&lt;/code&gt; complains about a native module&lt;/strong&gt; — reinstall with the correct build flags for your package manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model list empty but provider shows connected&lt;/strong&gt; — verify the key on the Endpoints page is the one you're sending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works non-streaming, breaks streaming&lt;/strong&gt; — check timeouts; streaming stalls are governed by &lt;code&gt;STREAM_IDLE_TIMEOUT_MS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback doesn't trigger&lt;/strong&gt; — confirm the broken provider actually fails with an error OmniRoute can act on (4xx/5xx), and that the combo has more than one step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten minutes of this checklist beats an hour of debugging why your agent silently "can't reach the model" in the middle of real work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>llm</category>
      <category>testing</category>
    </item>
    <item>
      <title>Testing Qwen3.8 Max on a Budget with OpenCode Go: 10 Tasks, ~10¢, Zero Failures</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Sat, 15 Aug 2026 13:21:01 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/testing-qwen38-max-on-a-budget-with-opencode-go-10-tasks-10c-zero-failures-5g12</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/testing-qwen38-max-on-a-budget-with-opencode-go-10-tasks-10c-zero-failures-5g12</guid>
      <description>&lt;p&gt;Qwen3.8 Max is the &lt;strong&gt;most expensive model on OpenCode Go&lt;/strong&gt; — &lt;strong&gt;$2.00/M input and $6.00/M output&lt;/strong&gt;, with the added twist of a &lt;strong&gt;$2.50/M cache-write&lt;/strong&gt; fee that most models don't charge. It ships with a fixed &lt;strong&gt;$15/month&lt;/strong&gt; usage allowance and only &lt;strong&gt;~160 requests per 5-hour window&lt;/strong&gt;, so every token — cached or not — counts.&lt;/p&gt;

&lt;p&gt;So I wanted to answer a simple question: &lt;em&gt;is the priciest model on the plan actually worth it?&lt;/em&gt; — without spending more than a coffee to find out.&lt;/p&gt;

&lt;p&gt;I built a tiny benchmark that cost &lt;strong&gt;under a dime&lt;/strong&gt; total. Here's what I did and what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget math
&lt;/h2&gt;

&lt;p&gt;Qwen3.8 Max has four price tiers, not the usual three:&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;Price (per 1M tokens)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$6.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached read&lt;/td&gt;
&lt;td&gt;$0.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cached write&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$2.50&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That cache-write fee is the unusual part — and it turned out to be the whole story (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;One line, same as any other Go model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;opencode run &lt;span class="nt"&gt;--model&lt;/span&gt; opencode-go/qwen3.8-max &lt;span class="s2"&gt;"your prompt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;I reused the exact same 10-prompt harness from my GLM-5.3 test — a Bash loop running each prompt through &lt;code&gt;opencode run&lt;/code&gt;, timing it and saving the output. The prompts span the categories a coding agent actually needs: code generation, bug fixing, code review, SQL, regex, logic, math, structured output, explanation, and format-following.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&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;Category&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Wall time&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;Codegen&lt;/td&gt;
&lt;td&gt;Flatten a nested list (iterative)&lt;/td&gt;
&lt;td&gt;✅ Correct stack-based solution&lt;/td&gt;
&lt;td&gt;16.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bug fix&lt;/td&gt;
&lt;td&gt;Binary search off-by-one&lt;/td&gt;
&lt;td&gt;✅ Caught the &lt;code&gt;return lo&lt;/code&gt; bug&lt;/td&gt;
&lt;td&gt;13.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Code review&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max()&lt;/code&gt; in Go&lt;/td&gt;
&lt;td&gt;✅ Found 3 real issues&lt;/td&gt;
&lt;td&gt;15.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Top-3 paid per department&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;DENSE_RANK()&lt;/code&gt; + tie handling&lt;/td&gt;
&lt;td&gt;8.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;Valid IPv4&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;8.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Logic&lt;/td&gt;
&lt;td&gt;8-ball / 2-weighing puzzle&lt;/td&gt;
&lt;td&gt;✅ Correct 3-3-2 strategy&lt;/td&gt;
&lt;td&gt;11.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;Derivative of &lt;code&gt;x³·ln x&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;x²(3·ln x + 1)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;✅ Valid JSON, no fences&lt;/td&gt;
&lt;td&gt;5.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Explain&lt;/td&gt;
&lt;td&gt;JS dedup snippet&lt;/td&gt;
&lt;td&gt;✅ Correct + 2 improvements&lt;/td&gt;
&lt;td&gt;8.0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Exact bullet-list format&lt;/td&gt;
&lt;td&gt;✅ Followed exactly&lt;/td&gt;
&lt;td&gt;6.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;10/10 correct.&lt;/strong&gt; Total wall time &lt;strong&gt;~99 seconds&lt;/strong&gt; (avg ~10s/task).&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It's fast because it doesn't "think"
&lt;/h3&gt;

&lt;p&gt;Unlike the reasoning models I've tested, Qwen3.8 Max produced &lt;strong&gt;zero reasoning tokens&lt;/strong&gt; — it answers directly. Every task completed in 5.5–16.6s. That's the trade-off this model makes: raw speed over deliberation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The cache-write surprise (the big one)
&lt;/h3&gt;

&lt;p&gt;This is what makes Qwen3.8 Max's cost non-obvious. Look at three requests I measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Cache write&lt;/th&gt;
&lt;th&gt;Cache read&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold cache (first request)&lt;/td&gt;
&lt;td&gt;10,508&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.027&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm cache (easy)&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;10,468&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.003&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm cache (hard)&lt;/td&gt;
&lt;td&gt;1,635&lt;/td&gt;
&lt;td&gt;11,584&lt;/td&gt;
&lt;td&gt;264&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.0086&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;first request is ~9× more expensive&lt;/strong&gt; than the ones that follow. Why? On a cold cache, OpenCode's ~10.5K-token system prompt gets &lt;em&gt;written&lt;/em&gt; to the cache at &lt;strong&gt;$2.50/M&lt;/strong&gt; (~$0.026). Once warm, that same prefix is &lt;em&gt;read&lt;/em&gt; back at &lt;strong&gt;$0.25/M&lt;/strong&gt; (~$0.0026) — a 10× discount.&lt;/p&gt;

&lt;p&gt;So the headline "$6.00/M output" barely matters for short prompts. The real cost is &lt;strong&gt;cache-write, one-time per cache lifetime&lt;/strong&gt;. In a sustained agentic session you pay it once and then cruise on cheap reads.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The quality bar is high
&lt;/h3&gt;

&lt;p&gt;The code review was the standout — it caught the all-negative-input bug, the empty-slice edge case, &lt;strong&gt;and&lt;/strong&gt; that the function shadows Go 1.21's built-in &lt;code&gt;max&lt;/code&gt;, suggesting the idiomatic &lt;code&gt;max(nums...)&lt;/code&gt;. That's a detail most junior devs (and several models) miss.&lt;/p&gt;

&lt;p&gt;Minor nitpick: the JSON task invented a name ("Jane Doe") when none was given — the same harmless hallucination I saw from other models on person-object prompts.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cost is dominated by caching, not difficulty
&lt;/h3&gt;

&lt;p&gt;Because there are no reasoning tokens, cost doesn't scale with problem difficulty the way it does on a reasoning model. Easy and hard tasks cost nearly the same — the variable is only output length (264 tokens max here) plus any &lt;em&gt;new&lt;/em&gt; tokens that miss the cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold-cache request (first ever)&lt;/td&gt;
&lt;td&gt;~$0.027&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm-cache request (typical)&lt;/td&gt;
&lt;td&gt;~$0.003–0.009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10-prompt suite total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.05&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against the $15 monthly allowance, the whole test was &lt;strong&gt;~0.3% of my monthly usage&lt;/strong&gt; — and I did it on the most expensive model on the plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;Qwen3.8 Max is a &lt;strong&gt;fast, direct, high-quality&lt;/strong&gt; model. 10/10 on a mixed coding suite at an average of 10s per task, for roughly a nickel.&lt;/p&gt;

&lt;p&gt;The catch isn't the sticker price — it's knowing how you're charged. If you fire single-shot prompts, you'll pay the $2.50/M cache-write penalty on every cold request and think the model is expensive. If you run it the way it's meant to be used — as a sustained agent inside OpenCode — the cache warms up and your effective cost collapses ~10×.&lt;/p&gt;

&lt;p&gt;For high-volume, latency-sensitive coding work, that makes Qwen3.8 Max quietly cost-effective. Just don't judge it by your first request.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Same open-source harness and prompts as my GLM-5.3 test. Total spend for this entire Qwen3.8 Max experiment: under **10 cents&lt;/em&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Testing GLM-5.3 on a Budget with OpenCode Go: 10 Tasks, 4¢, Zero Failures</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Fri, 14 Aug 2026 17:51:42 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/testing-glm-53-on-a-budget-with-opencode-go-10-tasks-4c-zero-failures-1j39</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/testing-glm-53-on-a-budget-with-opencode-go-10-tasks-4c-zero-failures-1j39</guid>
      <description>&lt;p&gt;On August 14, 2026, Zhipu dropped &lt;strong&gt;GLM-5.3&lt;/strong&gt; — a &lt;a href="https://z.ai/blog/glm-5.3" rel="noopener noreferrer"&gt;post-training-only upgrade over GLM-5.2&lt;/a&gt; with 128K max output tokens. I put it through its paces with OpenCode Go the same day — spoiler: it's awesome.&lt;/p&gt;

&lt;p&gt;It's one of the pricier models on the &lt;a href="https://opencode.ai/docs/go/" rel="noopener noreferrer"&gt;OpenCode Go&lt;/a&gt; plan — a reasoning model at &lt;strong&gt;$1.40/M input and $4.40/M output&lt;/strong&gt;, with a &lt;strong&gt;$15/month&lt;/strong&gt; usage allowance, so every token counts. I wanted to answer a simple question: &lt;em&gt;is it worth the premium over cheaper Go models like DeepSeek V4 Pro ($0.435/$0.87)?&lt;/em&gt; — without burning my monthly usage finding out.&lt;/p&gt;

&lt;p&gt;So I built a tiny benchmark that cost &lt;strong&gt;under 4 cents&lt;/strong&gt; total. Here's what I did and what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  The budget math
&lt;/h2&gt;

&lt;p&gt;Before writing a single prompt, I worked out what a request actually costs:&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;Price (per 1M tokens)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;$1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;$4.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached read&lt;/td&gt;
&lt;td&gt;$0.26&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A self-contained prompt sent through &lt;code&gt;opencode run&lt;/code&gt; (no tools, no file ops) is roughly &lt;strong&gt;50 input + 100–180 output tokens&lt;/strong&gt;. Even at the top end that's about &lt;strong&gt;$0.007 per request&lt;/strong&gt;. Ten prompts ≈ &lt;strong&gt;4 cents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The trick to staying cheap: short, self-contained prompts that don't trigger agentic tool loops, and a shared system prompt so OpenCode's context stays in the cache ($0.26/M instead of $1.40/M).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;One line. GLM-5.3 is already in the Go catalog under the model id &lt;code&gt;opencode-go/glm-5.3&lt;/code&gt;, so after &lt;code&gt;/connect&lt;/code&gt; → OpenCode Go it's just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;opencode run &lt;span class="nt"&gt;--model&lt;/span&gt; opencode-go/glm-5.3 &lt;span class="s2"&gt;"your prompt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;A Bash harness loops over 10 prompts and runs each one through &lt;code&gt;opencode run&lt;/code&gt;, timing it and saving the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MODEL&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;opencode&lt;/span&gt;&lt;span class="p"&gt;-go/glm-5.3&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SUFFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s1"&gt;Answer directly. Do not run any code or use any tools. Just provide the answer.'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"task,wall_seconds,status"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results/summary.csv
&lt;span class="nv"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; prompt&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  opencode run &lt;span class="nt"&gt;--model&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prompt$SUFFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"results/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%02d'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.md"&lt;/span&gt; 2&amp;gt;&amp;amp;1
  &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s.%N&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$idx&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | bc &lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;,ok"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; results/summary.csv
  &lt;span class="nv"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;idx+1&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys;print('&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;'.join([p.strip() for p in open('prompts.txt').read().split('=====') if p.strip()]))"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 10 prompts span the categories a coding agent actually needs: code generation, bug fixing, code review, SQL, regex, logic, math, structured output, explanation, and format-following.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&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;Category&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Wall time&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;Codegen&lt;/td&gt;
&lt;td&gt;Flatten a nested list (iterative)&lt;/td&gt;
&lt;td&gt;✅ Correct stack-based solution&lt;/td&gt;
&lt;td&gt;10.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Bug fix&lt;/td&gt;
&lt;td&gt;Binary search off-by-one&lt;/td&gt;
&lt;td&gt;✅ Caught the &lt;code&gt;return lo&lt;/code&gt; bug&lt;/td&gt;
&lt;td&gt;53.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Code review&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;max()&lt;/code&gt; in Go&lt;/td&gt;
&lt;td&gt;✅ Found 3 real issues&lt;/td&gt;
&lt;td&gt;15.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;Top-3 paid per department&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;DENSE_RANK()&lt;/code&gt; + tie handling&lt;/td&gt;
&lt;td&gt;45.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Regex&lt;/td&gt;
&lt;td&gt;Valid IPv4&lt;/td&gt;
&lt;td&gt;✅ Correct&lt;/td&gt;
&lt;td&gt;21.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Logic&lt;/td&gt;
&lt;td&gt;8-ball / 2-weighing puzzle&lt;/td&gt;
&lt;td&gt;✅ Correct 3-3-2 strategy&lt;/td&gt;
&lt;td&gt;28.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Math&lt;/td&gt;
&lt;td&gt;Derivative of &lt;code&gt;x³·ln x&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;x²(3·ln x + 1)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;24.8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;✅ Valid JSON, no fences&lt;/td&gt;
&lt;td&gt;28.7s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Explain&lt;/td&gt;
&lt;td&gt;JS dedup snippet&lt;/td&gt;
&lt;td&gt;✅ Correct + 2 improvements&lt;/td&gt;
&lt;td&gt;35.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Format&lt;/td&gt;
&lt;td&gt;Exact bullet-list format&lt;/td&gt;
&lt;td&gt;✅ Followed exactly&lt;/td&gt;
&lt;td&gt;8.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;10/10 correct.&lt;/strong&gt; Total wall time ~4.5 minutes. Total cost &lt;strong&gt;~$0.04&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It's a reasoning model — and it thinks &lt;em&gt;a lot&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This was the surprise. GLM-5.3 emits &lt;strong&gt;thinking tokens&lt;/strong&gt; before answering, and the amount of thinking scales with task difficulty:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Reasoning tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"List three benefits…" (easy)&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL top-3 (medium)&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flatten nested list (hard)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;749&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the hard task it thought ~4× more than it wrote. That reasoning is billed as &lt;strong&gt;output&lt;/strong&gt; ($4.40/M), so it's the single biggest cost driver — and the biggest latency driver too.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Latency is the real price, not dollars
&lt;/h3&gt;

&lt;p&gt;Wall times ranged from &lt;strong&gt;8s to 53s&lt;/strong&gt;. The binary-search fix (53s) and SQL query (45s) were the slowest, both reasoning-heavy. For a $4.40/M output model, the money is trivial — the &lt;em&gt;waiting&lt;/em&gt; is what you'll notice in day-to-day use.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. There's a hidden reasoning dial
&lt;/h3&gt;

&lt;p&gt;When I hit GLM-5.3's raw OpenAI-compatible endpoint directly with default settings, it went off the rails: it burned &lt;strong&gt;2,047 reasoning tokens on "flatten a list"&lt;/strong&gt; and produced an &lt;em&gt;empty&lt;/em&gt; answer because it hit the token cap while still thinking, with a &lt;strong&gt;49-second time-to-first-token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Passing &lt;code&gt;"reasoning_effort": "low"&lt;/code&gt; changed everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;low  =&amp;gt; finish=stop  reasoning=2   total=6   content="444"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenCode Go's default tuning keeps the model usable out of the box, but if you're calling the API yourself, you'll want to set a reasoning effort explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The quality bar is genuinely high
&lt;/h3&gt;

&lt;p&gt;The code review was the highlight — it caught the all-negative-input bug, the empty-slice edge case, &lt;strong&gt;and&lt;/strong&gt; that the function shadows Go 1.21's built-in &lt;code&gt;max&lt;/code&gt;, then pointed at &lt;code&gt;slices.Max()&lt;/code&gt;. That's the kind of detail I'd expect from a flagship model.&lt;/p&gt;

&lt;p&gt;Minor nitpicks: the Go fix used &lt;code&gt;fmt.Errorf&lt;/code&gt; without importing &lt;code&gt;fmt&lt;/code&gt;, and the JSON task invented a name ("Ahmed Hassan") when none was given. Both trivial.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost breakdown
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request type&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Easy prompt (list three)&lt;/td&gt;
&lt;td&gt;$0.0030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium prompt (SQL)&lt;/td&gt;
&lt;td&gt;$0.0035&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard prompt (flatten)&lt;/td&gt;
&lt;td&gt;$0.0067&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;10-prompt suite total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.04&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Against a $15 monthly Go allowance, that's &lt;strong&gt;0.27% of my monthly usage&lt;/strong&gt; for a complete capability picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;GLM-5.3 passed every task with detail I'd call &lt;em&gt;frontier-grade&lt;/em&gt;, at a cost that's basically free on the Go plan. The trade-off is &lt;strong&gt;latency&lt;/strong&gt;: it's a deliberate thinker, so it's best for hard, non-trivial tasks rather than rapid-fire edits.&lt;/p&gt;

&lt;p&gt;If you want a fast, cheap daily driver, DeepSeek V4 Pro on Go is the better value. If you want a model that reasons through the hard stuff and rarely gets it wrong, GLM-5.3 earns its premium — and you can test it yourself for the price of a gumball.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All prompts, the harness, and raw results are in the repo. Total spend for this entire experiment (including the failed raw-API probes): under **8 cents&lt;/em&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why GPT-5.6 Luna High Is My Default for Agentic Engineering</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Fri, 14 Aug 2026 17:38:34 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/why-gpt-56-luna-high-is-my-default-for-agentic-engineering-4e04</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/why-gpt-56-luna-high-is-my-default-for-agentic-engineering-4e04</guid>
      <description>&lt;p&gt;I used to pick coding models the same way people pick sports cars: choose the most powerful one and pretend the fuel bill is somebody else's problem.&lt;/p&gt;

&lt;p&gt;That worked when I was asking one question at a time. Then I started using agents for real engineering work.&lt;/p&gt;

&lt;p&gt;An engineering agent does not answer once and disappear. It reads the repository, searches for related code, opens the wrong file, finds the right file, proposes a change, runs a test, breaks something, reads the error, fixes the change, and runs the test again. Sometimes it also writes a surprisingly thoughtful essay about the three lines it just modified.&lt;/p&gt;

&lt;p&gt;By the time one task is finished, the model may have been called a dozen times. Suddenly, model pricing is not a footnote. It is part of the architecture.&lt;/p&gt;

&lt;p&gt;That is how GPT-5.6 Luna with high reasoning effort became my default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheap model that gets the job done
&lt;/h2&gt;

&lt;p&gt;OpenAI positions Luna for cost-sensitive, high-volume work, and the pricing makes that very clear. As of August 2026, GPT-5.6 Luna costs $0.20 per million input tokens and $1.20 per million output tokens. Claude Sonnet 5 costs $2 for input and $10 for output.&lt;/p&gt;

&lt;p&gt;So Luna's input is ten times cheaper, and its output is about 8.3 times cheaper.&lt;/p&gt;

&lt;p&gt;Take an agent run that consumes 100,000 input tokens and produces 20,000 output tokens. Luna costs about $0.044. Sonnet costs about $0.40. Neither amount is terrifying on its own, but agents are enthusiastic employees. Give them access to a repository and they will inspect everything, call every tool, and occasionally create a strategic roadmap for changing a variable name.&lt;/p&gt;

&lt;p&gt;Repeat that across pull requests, bug fixes, failed CI jobs, and background tasks, and the difference stops being theoretical.&lt;/p&gt;

&lt;p&gt;Using Luna means I can let the agent iterate without feeling that every failed test needs approval from Finance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I use high reasoning effort
&lt;/h2&gt;

&lt;p&gt;Cheap is useful only if the model can finish the work. I do not run Luna with minimal reasoning and hope for a miracle. I use high reasoning effort.&lt;/p&gt;

&lt;p&gt;That setting gives it enough room to trace behavior across files, understand unfamiliar abstractions, choose tools, react to command failures, and verify the result before declaring victory. In my experience, that is enough for a large part of normal engineering work.&lt;/p&gt;

&lt;p&gt;It is not the smartest model available, and I do not need it to be. My default model does not have to solve every impossible architecture problem. It has to handle routine features, localized bugs, tests, refactoring, and repository exploration without turning each task into an expensive research project.&lt;/p&gt;

&lt;p&gt;There is a temptation to treat "accurate enough" as an insult. I see it differently. Most engineering tasks do not need a model to contemplate the nature of software. They need it to follow the repository instructions, make a focused change, preserve unrelated code, understand the compiler output, and actually run the tests.&lt;/p&gt;

&lt;p&gt;Luna does that well enough for me, especially when the agent harness is doing its job. Good context, sensible permissions, small changes, and real test feedback matter just as much as the model. A cheaper model inside a disciplined workflow can be more useful than an expensive model confidently wandering through the codebase.&lt;/p&gt;

&lt;p&gt;Speed matters too. A small delay is easy to ignore in one response. Across fifteen sequential agent turns, it becomes enough time to question your career choices. Luna keeps the loop moving. It can inspect, attempt, fail, correct, and verify without making every step feel like a separate meeting.&lt;/p&gt;

&lt;h2&gt;
  
  
  And what about Claude Sonnet?
&lt;/h2&gt;

&lt;p&gt;Claude Sonnet is a capable model. There are difficult tasks where I am happy to use it. It is simply not my default.&lt;/p&gt;

&lt;p&gt;The obvious reason is price. The less obvious reason is output efficiency. In my experience, Sonnet often gives me more explanation than the engineering loop needs. I ask for a patch and sometimes receive the patch, a retrospective, and the opening chapter of a management book.&lt;/p&gt;

&lt;p&gt;That can be useful when I am exploring a decision. It is less useful when the agent is supposed to edit a file and run the tests. Verbose output is not only more to read; output tokens are also the expensive tokens.&lt;/p&gt;

&lt;p&gt;Anthropic's documentation says Claude 4.7 and later can produce roughly 30% more tokens for the same text than models using its previous tokenizer, although the exact difference depends on the workload. That is not the same as saying the model is always verbose, but it is another reminder that token behavior belongs in the engineering decision.&lt;/p&gt;

&lt;p&gt;For autonomous execution, I care more about correct edits and passing tests than an eloquent account of the model's emotional journey.&lt;/p&gt;

&lt;p&gt;My approach is simple: start with Luna High and escalate when the task earns it. If the architecture is genuinely ambiguous, failures remain unexplained, or the work involves subtle security or concurrency behavior, I will reach for a stronger model. That is not model loyalty. It is routing.&lt;/p&gt;

&lt;p&gt;The best model is not always the one that wins the benchmark. For agentic engineering, I care about the cost of the completed task, how long it took, how many retries it needed, whether the tests passed, and how much cleanup I had to do afterward.&lt;/p&gt;

&lt;p&gt;For my workload, GPT-5.6 Luna with high reasoning effort currently hits the useful middle: very cheap, very fast, and accurate enough to handle the common path.&lt;/p&gt;

&lt;p&gt;It is not the model I want solving every problem. It is the model I want holding the wrench most of the time.&lt;/p&gt;




&lt;p&gt;Pricing sources: &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;OpenAI model documentation&lt;/a&gt; and &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic pricing documentation&lt;/a&gt;, checked August 14, 2026.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>softwareengineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Asked Muse Glimmer to Pair Program With Me in OpenCode</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Tue, 11 Aug 2026 22:04:13 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/i-asked-muse-glimmer-to-pair-program-with-me-in-opencode-kkh</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/i-asked-muse-glimmer-to-pair-program-with-me-in-opencode-kkh</guid>
      <description>&lt;p&gt;I wanted to use Meta's Muse Glimmer as a local agentic engineering assistant inside OpenCode.&lt;/p&gt;

&lt;p&gt;Not as a chatbot in a browser. I wanted an agent that could inspect a repository, understand a task, edit files, run tests, and explain what it did.&lt;/p&gt;

&lt;p&gt;The command was refreshingly short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model, however, is a 30-billion-parameter coworker who arrives carrying an approximately 18 GB suitcase and expects a 128K-context office.&lt;/p&gt;

&lt;h2&gt;
  
  
  The onboarding meeting
&lt;/h2&gt;

&lt;p&gt;First, I checked Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I started the local service if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified that the local API was alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:11434/api/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I launched OpenCode with Muse Glimmer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run downloads the model. This is a good opportunity to make coffee, reconsider your disk space, and remember that “local AI” does not mean “tiny AI.”&lt;/p&gt;

&lt;p&gt;To inspect installed models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Apple Silicon, Ollama also lists an MLX variant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer:30b-mlx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The standard model is listed at around 18 GB. That is the model's luggage, not the entire hotel bill. Leave memory for the context, OpenCode, the operating system, and the repository you are asking the agent to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenCode changes the experiment
&lt;/h2&gt;

&lt;p&gt;The interesting part is not just running Muse Glimmer locally. It is putting the model inside an engineering workflow.&lt;/p&gt;

&lt;p&gt;With OpenCode, I can ask the agent to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inspect the repository.&lt;/li&gt;
&lt;li&gt;Find the relevant files.&lt;/li&gt;
&lt;li&gt;Explain the current behavior.&lt;/li&gt;
&lt;li&gt;Propose a plan.&lt;/li&gt;
&lt;li&gt;Make a focused change.&lt;/li&gt;
&lt;li&gt;Run the project's tests.&lt;/li&gt;
&lt;li&gt;Read failures.&lt;/li&gt;
&lt;li&gt;Revise the patch.&lt;/li&gt;
&lt;li&gt;Show the final diff and verification result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the difference between asking a model for code and asking an agent to help complete an engineering task. The model can still be confidently wrong. The workflow gives me more chances to catch it before the wrongness becomes a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first repository tour
&lt;/h2&gt;

&lt;p&gt;I opened the repository in OpenCode and started with a read-only request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are working in a local software repository. Inspect the project structure and identify:

- The application entry point
- The test command
- The main source directories
- Configuration files that affect development
- Any obvious contribution or safety constraints

Do not modify files. Report the evidence and ask before making changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a better first task than “fix everything.” It tests whether Muse Glimmer can build context without immediately turning the repository into modern art.&lt;/p&gt;

&lt;p&gt;The key signal is whether the response points to actual files and commands. A useful agent should report evidence, not invent a project architecture because the folder name sounded familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first engineering task
&lt;/h2&gt;

&lt;p&gt;After the repository tour, I gave OpenCode a small, testable issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find the bug in the relevant average function. Explain the edge case, propose the smallest fix, and add a regression test. Do not change unrelated files.

The current implementation is: average(values) = sum(values) / len(values) + 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extra &lt;code&gt;+ 1&lt;/code&gt; is suspicious. It is the kind of bug that enters production wearing sunglasses and saying, “Nobody will notice.”&lt;/p&gt;

&lt;p&gt;A good OpenCode session should locate the real implementation, find the project's existing test style, make the smallest patch, and run the relevant test. A locally generated patch is still a patch. It needs review, tests, and version control like any other change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning before editing
&lt;/h2&gt;

&lt;p&gt;I asked OpenCode to separate investigation from implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before editing, write a short implementation plan with:

- Files you expect to inspect
- The behavior you expect to change
- The test you will add or update
- The command you will run to verify the change

Wait for approval before modifying files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a useful checkpoint. It also prevents the classic coding-agent experience where you ask for one small fix and receive a surprise framework migration, a new abstraction layer, and a README poem.&lt;/p&gt;

&lt;p&gt;OpenCode is the interface where this loop becomes practical: inspect the plan, approve the direction, watch the diff, and ask for verification in the same engineering session.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test-and-repair loop
&lt;/h2&gt;

&lt;p&gt;The useful workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect -&amp;gt; plan -&amp;gt; approve -&amp;gt; edit -&amp;gt; test -&amp;gt; inspect failure -&amp;gt; revise -&amp;gt; test again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested failure recovery with an intentionally broken test result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The test runner returned:

ERROR: assertion failed in tests/test_average.py
Expected: 2.0
Received: 3.0

Explain the failure, identify the likely cause, and propose the smallest correction. Do not edit files yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I tested a repository constraint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The repository check returned:

ERROR: the generated lockfile differs from the committed lockfile.

Do not regenerate dependencies. Explain what should be reviewed first.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to make the agent retry everything until the terminal turns green. The goal is to see whether it can distinguish a fixable implementation error from a situation requiring human review.&lt;/p&gt;

&lt;p&gt;“The model usually behaves” is not an engineering control. It is something you say immediately before opening an incident ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot-driven debugging
&lt;/h2&gt;

&lt;p&gt;Muse Glimmer supports image input, which is useful for UI bugs, screenshots, diagrams, and terminal output. From the local OpenCode workflow, I can give the agent a screenshot and ask for evidence-first analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inspect this screenshot as a debugging artifact. List only visible symptoms, separate observations from hypotheses, and suggest which source files or browser checks should be inspected next.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“The button is clipped” is an observation. “The CSS grid is broken because of a missing min-width” is a hypothesis. That distinction saves time and prevents a screenshot from becoming a license to rewrite the frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Using Muse Glimmer with OpenCode through Ollama is straightforward to start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama launch opencode &lt;span class="nt"&gt;--model&lt;/span&gt; muse-glimmer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting work begins after OpenCode starts. Ask the agent to inspect, plan, edit, test, diagnose, and explain.&lt;/p&gt;

&lt;p&gt;Muse Glimmer can be the clever local coworker. Ollama handles the model runtime. OpenCode handles the engineering conversation. You remain the person who decides whether the patch deserves to live.&lt;/p&gt;

&lt;p&gt;Resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://research.meta.ai/blog/introducing-muse-glimmer-open-agentic-model" rel="noopener noreferrer"&gt;Muse Glimmer announcement from Meta AI Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ollama.com/library/muse-glimmer" rel="noopener noreferrer"&gt;Muse Glimmer on Ollama&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ollama.com/integrations/opencode" rel="noopener noreferrer"&gt;Ollama's OpenCode integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://opencode.ai/" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>opencode</category>
      <category>coding</category>
    </item>
    <item>
      <title>I Let an Autonomous Agent Build My RAG Stack. Here's How the Loop Worked</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Thu, 06 Aug 2026 17:24:49 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/i-let-an-autonomous-agent-build-my-rag-stack-heres-how-the-loop-worked-520j</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/i-let-an-autonomous-agent-build-my-rag-stack-heres-how-the-loop-worked-520j</guid>
      <description>&lt;h1&gt;
  
  
  I Let an Autonomous Agent Build My RAG Stack. Here's How the Loop Worked
&lt;/h1&gt;

&lt;p&gt;I didn't write a single line of code. I didn't debug a Helm chart. I didn't stare at a traceback wondering why the connector wouldn't authenticate.&lt;/p&gt;

&lt;p&gt;I typed one sentence and watched.&lt;/p&gt;

&lt;p&gt;Twenty minutes later, a draft pull request landed in my private repo. Everything behind that PR — the OpenSearch cluster, the embedding models, the five init jobs in lockstep, the MCP server, the RAG pipeline, and the 30 seeded articles — was produced by an autonomous coding agent using a workflow called &lt;strong&gt;the loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The stack itself is impressive enough: OpenSearch 3.7 with hybrid search fusing semantic and full-text results, a retrieval-augmented generation pipeline powered by a local Ollama LLM, a FastMCP server exposing four tools over SSE, and a Skaffold + Helm deployment that goes from zero to running cluster with &lt;code&gt;skaffold run&lt;/code&gt;. But the stack isn't the story.&lt;/p&gt;

&lt;p&gt;The story is what happened between the prompt and the PR. The planning, the self-correction, the moment the agent deleted 14 model chunks it thought were duplicates, broke the entire embedding system, and then — without any help — fixed it.&lt;/p&gt;

&lt;p&gt;This is how the loop worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop isn't just an agent. It's a contract.
&lt;/h2&gt;

&lt;p&gt;The loop is a skill for &lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;opencode&lt;/a&gt; that turns a natural-language prompt into a working feature branch and a draft PR. It sounds like every other AI coding tool until you see what it &lt;em&gt;won't&lt;/em&gt; do: it won't write code before you approve a plan.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;proposal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Explores the codebase, analyzes the prompt, proposes a plan. No implementation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;agreement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You review. You can push back, ask questions, or say go. The agent waits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;implementation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Builds the agreed scope on a feature branch, committing early and often.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;draft_gate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-review: lint, tests, diff inspection. Fixes issues before you ever see them.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;draft_pr&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pushes the branch, opens a draft PR, reports back. Done.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every agent should work like this. Most don't. Most just start writing code the moment you press enter, and you spend the rest of the session saying "no, not like that."&lt;/p&gt;

&lt;p&gt;The loop carries its state in &lt;code&gt;.loop-state.json&lt;/code&gt; — the current phase, the branch, the PR number, the plan, and a log of every phase transition. If your session crashes or you walk away for an hour, the loop resumes exactly where it left off. This alone makes it viable for multi-hour autonomous runs.&lt;/p&gt;

&lt;p&gt;Then there's &lt;strong&gt;loop-police&lt;/strong&gt;: a companion skill that rides shotgun, watching for infinite loops. Thinking loops. Tool-call loops. File-read spirals. Stagnation. When loop-police detects the agent going in circles, it interrupts: &lt;em&gt;"You're stuck. Pivot."&lt;/em&gt; That intervention is the difference between an agent that wastes ten turns retrying the same failing deploy and one that recognizes the failure and changes strategy.&lt;/p&gt;

&lt;p&gt;Without loop-police, the chunk-deletion disaster I'll get to later might have been unrecoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plan was one paragraph
&lt;/h2&gt;

&lt;p&gt;The agent's proposal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build &lt;code&gt;opensearch-hybrid-mcp&lt;/code&gt; mirroring the &lt;code&gt;os-hybrid-search&lt;/code&gt; reference: OpenSearch 3.7 with Dashboards, MCP server (FastMCP + UV), five init jobs (template → embedding → connector → pipeline → seed), &lt;code&gt;skaffold.yaml&lt;/code&gt; with six images, &lt;code&gt;justfile&lt;/code&gt;, README. Private repo + draft PR.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then a diagram — the agent drew the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS host                             Kubernetes (Colima)
┌───────────────┐                      ┌────────────────────────────────────────────────────────────┐
│ Ollama        │                      │  ┌──────────────────┐  ┌────────────────┐                  │
│ (Desktop)     │◄──────── :11434 ────►│  │  OpenSearch 3.7  │  │  OS Dashboards │                  │
│ qwen3.5:9b-mlx│  host.docker.internal│  │  os-hybrid:9200  │  │  :5601         │                  │
└───────────────┘                      │  └────────┬─────────┘  └────────────────┘                  │
                                       │          │                                                 │
                                       │  ┌────────▼──────────┐                                     │
                                       │  │  MCP Server       │    FastMCP + UV                     │
                                       │  │  mcp-server:8000  │    hybrid_search                    │
                                       │  │                   │    rag_query                        │
                                       │  │                   │    index_document                   │
                                       │  │                   │    cluster_health                   │
                                       │  └───────────────────┘                                     │
                                       │                                                            │
                                       │  Init Jobs (run in sequence):                              │
                                       │    1. template-job    index template                       │
                                       │    2. embedding-job   all-MiniLM-L12-v2                    │
                                       │    3. connector-job   Ollama → Qwen 3.5                    │
                                       │    4. pipeline-job    chunking/hybrid/RAG                  │
                                       │    5. seed-job        30 articles                          │
                                       └────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama runs on the host, not inside the cluster — pods reach it through &lt;code&gt;host.docker.internal:11434&lt;/code&gt;. The agent figured that out by itself. Single-node OpenSearch, 10Gi PVC, no security (it's a dev cluster), MCP server on SSE port 8000. Five init jobs that run in sequence, each waiting for its upstream dependencies through retry loops.&lt;/p&gt;

&lt;p&gt;I said yes. The agent started building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation was fast. The recovery was where it got interesting.
&lt;/h2&gt;

&lt;p&gt;Seven commits. Forty-plus files. The agent scaffolded the entire project in minutes.&lt;/p&gt;

&lt;p&gt;Six Helm charts (OpenSearch parent, MCP server, five init jobs). A &lt;code&gt;skaffold.yaml&lt;/code&gt; with six Docker images building in parallel. A &lt;code&gt;justfile&lt;/code&gt; with shortcuts for &lt;code&gt;skaffold run&lt;/code&gt;, &lt;code&gt;kubectl port-forward&lt;/code&gt;, and job logs. Every init job is a shell script with &lt;code&gt;until&lt;/code&gt; loops — each waits for OpenSearch, for the index template, for the embedding model to deploy, for the pipeline to be ready. If a dependency isn't there, the job pauses. If it is, the job runs. This makes the whole stack re-runnable: delete a job and re-create it, and it picks up where it left off.&lt;/p&gt;

&lt;p&gt;There's a satisfying recursion here. The loop is a five-phase workflow of gates and retries. The jobs it wrote are retry loops with dependency checks. The agent built code that mirrors its own architecture.&lt;/p&gt;

&lt;p&gt;The MCP server landed next: FastMCP, four tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hybrid_search(query, index?, k?)&lt;/code&gt; — neural + full-text, z-score fused&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rag_query(question, index?, k?)&lt;/code&gt; — retrieves context, generates answer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;index_document(id, title, body, index?)&lt;/code&gt; — chunks + embeds + indexes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cluster_health()&lt;/code&gt; — cluster status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent ran its own draft gate: &lt;code&gt;helm lint&lt;/code&gt; and &lt;code&gt;helm template&lt;/code&gt; on seven charts, &lt;code&gt;skaffold render&lt;/code&gt;, &lt;code&gt;sh -n&lt;/code&gt; on every script. Clean. It pushed the branch, opened draft PR #1, and reported back.&lt;/p&gt;

&lt;p&gt;Technically, the loop was done: proposal → agreement → implementation → draft_gate → draft_pr. Five phases, zero handoffs.&lt;/p&gt;

&lt;p&gt;But the stack hadn't been deployed yet. And that's where everything went sideways — then sideways again — then somehow straightened out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five bugs. One autonomous agent. Zero panicking.
&lt;/h2&gt;

&lt;p&gt;The first &lt;code&gt;skaffold run&lt;/code&gt; kicked off and immediately hit a wall. The agent didn't wait for me to notice. It read the logs, diagnosed the problem, patched it, and redeployed. Five times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — "Connector credential is null or empty list"&lt;/strong&gt;: OpenSearch 3.7 demands a credential object even for local Ollama endpoints that don't use authentication. The agent added a dummy key (&lt;code&gt;openAI_key: "ollama-local"&lt;/code&gt;) and — crucially — switched the connector from Ollama's native &lt;code&gt;/api/chat&lt;/code&gt; to the OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt;, which is what the RAG processor expects. Two fixes in one pass. Redeployed. Connector created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2 — The one-character regex disaster&lt;/strong&gt;: RAG queries hit a 400: &lt;code&gt;"Connector URL is not matching the trusted connector private endpoint regex"&lt;/code&gt;. The agent had written &lt;code&gt;^http://host.docker.internal:11434:.*$&lt;/code&gt; — a colon after the port. The actual URL was &lt;code&gt;http://host.docker.internal:11434/v1/chat/completions&lt;/code&gt; — a slash. The difference between &lt;code&gt;:&lt;/code&gt; and &lt;code&gt;/&lt;/code&gt; broke the entire pipeline. The agent found it, flipped the character, and redeployed. The kind of bug a human would spend 20 minutes on. The agent caught it in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3 — "Model 'ollama-qwen3.5' not found"&lt;/strong&gt;: The connector was passing the OpenSearch-registered model name to Ollama, but Ollama expects the raw model name (&lt;code&gt;qwen3.5:9b-mlx&lt;/code&gt;). The agent traced the call chain — OpenSearch RAG processor → connector → Ollama API — and updated the MCP server to pass the correct model name in &lt;code&gt;ext.generative_qa_parameters&lt;/code&gt;. Fixed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 4 — Memory circuit breaker&lt;/strong&gt;: Seed-job bulk indexing hit 429 rate limits. JVM heap at 93% on 2g allocation. The agent bumped it to 4g (&lt;code&gt;-Xmx4g -Xms4g&lt;/code&gt;, container limits 4g/8Gi), redeployed, and the jobs succeeded. The agent understood enough about JVM memory pressure to recognize the root cause without being told.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 5 — The chunk-deletion disaster&lt;/strong&gt;: This is the one where the agent almost nuked the project. While cleaning up orphaned model registrations, it deleted 14 documents from the ML system index — thinking they were duplicates. They weren't. They were the embedding model's chunks (&lt;code&gt;_0&lt;/code&gt; through &lt;code&gt;_13&lt;/code&gt;). The model went to &lt;code&gt;DEPLOY_FAILED&lt;/code&gt;. The embedding pipeline was dead.&lt;/p&gt;

&lt;p&gt;Most agents would either not notice, or notice and keep retrying the same broken state until you killed the session. The loop did something else: it &lt;em&gt;recognized&lt;/em&gt; the mistake. It saw &lt;code&gt;DEPLOY_FAILED&lt;/code&gt; on the model, understood that the chunks it deleted were critical, deleted the broken base model document, let the &lt;code&gt;embedding-job&lt;/code&gt; re-register fresh with a new model ID, and re-ran the pipeline and seed jobs. The stack healed itself.&lt;/p&gt;

&lt;p&gt;That pattern — deploy, hit error, read logs, diagnose, patch, redeploy, verify — repeated five times. The agent operated the stack as well as it built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it worked
&lt;/h2&gt;

&lt;p&gt;After the last fix, the cluster settled. Thirty articles seeded. Both models deployed. Cluster health green.&lt;/p&gt;

&lt;p&gt;A hybrid search returned ranked results with fused neural and full-text scores. A RAG query against the pipeline — retrieves context first, then calls Ollama through the connector, returns the answer grounded in the retrieved documents:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inverted index TF-IDF BM25"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ext"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"generative_qa_parameters"&lt;/span&gt;&lt;span class="p"&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="nl"&gt;"llm_model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"qwen3.5:9b-mlx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"llm_question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Based on the context, explain what an inverted index is and how TF-IDF or BM25 ranking works."&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="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An inverted index is a structure that maps each unique word to the documents containing it, allowing for fast retrieval based on query terms. Regarding ranking, TF-IDF measures term frequency versus inverse document frequency, while BM25 improves upon this method by saturating term frequency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Grounded. Accurate. Sourced from the 30 articles the agent itself had seeded. All four MCP tools exercised and verified over SSE. The agent updated the draft PR with the validation results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently (and what I won't)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Loop-police is not optional.&lt;/strong&gt; Without it, the agent would have retried that failing deploy until the heat death of the universe. With it, the agent flagged the &lt;code&gt;DEPLOY_FAILED&lt;/code&gt; state transition, recognized it had caused the failure, and pivoted to recovery. If you're running autonomous agents, you need a stall detector. Period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agreement gate is the whole point.&lt;/strong&gt; Autonomous coding agents without a plan-agreement phase are just hyperactive interns with commit access. The loop doesn't move from proposal to implementation until you say yes. That one rule eliminates the most common failure mode of AI coding: the agent building the wrong thing while you watch helplessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State files make long-running sessions possible.&lt;/strong&gt; &lt;code&gt;.loop-state.json&lt;/code&gt; tracks phase, branch, PR number, and plan history. If the session drops — and with tools running 40+ minute autonomous sequences, it will — the loop resumes without context loss. This is infrastructure, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry-loop init jobs are self-healing infrastructure.&lt;/strong&gt; Each job waits for its dependencies through &lt;code&gt;until&lt;/code&gt; loops. You can tear down a job and recreate it — it'll wait, detect its prerequisites are met, and proceed. The loop wrote infrastructure that works the way the loop itself works. That's either poetic or recursive. Probably both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The draft gate is the QA step you'll never do yourself.&lt;/strong&gt; The loop ran &lt;code&gt;helm lint&lt;/code&gt;, &lt;code&gt;helm template&lt;/code&gt;, &lt;code&gt;skaffold render&lt;/code&gt;, and &lt;code&gt;sh -n&lt;/code&gt; before opening a PR. No human on my team does that for every commit. The agent does it because the workflow demands it. Fail the gate, and the loop doesn't proceed to PR. This catches broken manifests before they reach the repo — before you even see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop isn't magic. It's process.
&lt;/h2&gt;

&lt;p&gt;That's the real takeaway. Autonomous coding agents don't fail because they're not smart enough. They fail because they lack structure — no plan phase, no review gate, no self-check, no stall detection.&lt;/p&gt;

&lt;p&gt;The loop adds that structure. It turns an agent from a code generator into a collaborator: you get a plan, you approve it, the agent builds, it self-corrects, and it ships a draft PR. You review, you merge, you move on.&lt;/p&gt;

&lt;p&gt;I didn't write a single line of code. But the loop didn't build my RAG stack by accident. It built it because the process forced it to plan, verify, and recover — the same way a good engineer would.&lt;/p&gt;

&lt;p&gt;That's how the loop worked.&lt;/p&gt;

</description>
      <category>opensearch</category>
      <category>rag</category>
      <category>ai</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Building a 100% Local RAG System on Kubernetes — No API Keys Required</title>
      <dc:creator>Ahmed Nafies</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:00:55 +0000</pubDate>
      <link>https://dev.to/ahmed_nafies_3a55c907115c/building-a-100-local-rag-system-on-kubernetes-no-api-keys-required-4gg3</link>
      <guid>https://dev.to/ahmed_nafies_3a55c907115c/building-a-100-local-rag-system-on-kubernetes-no-api-keys-required-4gg3</guid>
      <description>&lt;p&gt;Here's what we're building:&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%2Fcr0h0j0gtptnpmbs5ald.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%2Fcr0h0j0gtptnpmbs5ald.png" alt="RAG UI Screenshot"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The itch I couldn't scratch
&lt;/h2&gt;

&lt;p&gt;Last week I sat down to build a RAG system. You know, one of those "chat with your documents" things everyone's building these days.&lt;/p&gt;

&lt;p&gt;I opened my first tutorial. It said: &lt;em&gt;"Step 1: Sign up for OpenAI and grab your API key."&lt;/em&gt; Fine. &lt;em&gt;"Step 2: Create a Pinecone account."&lt;/em&gt; Okay. &lt;em&gt;"Step 3: Deploy to a managed vector database."&lt;/em&gt; Hmm. &lt;em&gt;"Step 4: Set up your billing — here's a pricing calculator."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I stared at the calculator. Then at my $0 budget. Then back at the calculator.&lt;/p&gt;

&lt;p&gt;Something felt off. Why does a "personal project" need a procurement department? Why does my data need to leave my laptop to answer a question about a document sitting &lt;em&gt;on my laptop&lt;/em&gt;? And honestly — I just wanted to tinker. Not negotiate with three SaaS vendors before I could write a single line of code.&lt;/p&gt;

&lt;p&gt;So I did what any reasonable engineer would do. I closed all twenty tabs, opened a terminal, and decided to build the whole thing from scratch. On my machine. With models running on my CPU. No cloud. No API keys. No credit card.&lt;/p&gt;

&lt;p&gt;Here's how that ride went.&lt;/p&gt;




&lt;h2&gt;
  
  
  The game plan
&lt;/h2&gt;

&lt;p&gt;Before writing a line of code, I sketched out what "no cloud" actually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; — gotta run locally. No calling OpenAI's embedding endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search&lt;/strong&gt; — needs to live inside something I already run. I'm not spinning up a separate vector database service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM&lt;/strong&gt; — must fit in my laptop's RAM and run on CPU. I don't have a GPU lying around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt; — real infrastructure patterns, not &lt;code&gt;docker-compose up&lt;/code&gt; and pray. Something that would make sense in production, just scaled down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What emerged was this stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────┐
│                 FastAPI Application                 │
├────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────┐  ┌──────────────┐  │
│  │  Embedding   │  │ pgvector │  │  llama-cpp   │  │
│  │  Pipeline    │  │  Search  │  │  (Qwen 1.5B) │  │
│  └──────────────┘  └──────────┘  └──────────────┘  │
└────────────────────────────────────────────────────┘
                         │
                         ▼
               ┌──────────────────┐
               │   PostgreSQL     │
               │   + pgvector     │
               │   + HNSW index   │
               └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow: someone asks a question → it gets turned into a 384-dimensional vector → PostgreSQL hunts for similar documents using cosine similarity → those results get stuffed into a prompt → the LLM answers → answer streams back. All inside Kubernetes. All local.&lt;/p&gt;

&lt;p&gt;Every piece of this puzzle was chosen with one rule: &lt;strong&gt;it runs on my machine, or it doesn't make the cut.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the players
&lt;/h2&gt;

&lt;h3&gt;
  
  
  pgvector: the database you already have
&lt;/h3&gt;

&lt;p&gt;I wanted vector search without installing Yet Another Database. pgvector is a PostgreSQL extension that adds vector columns, indexing, and similarity operators directly into Postgres. Just &lt;code&gt;CREATE EXTENSION IF NOT EXISTS vector&lt;/code&gt; and you're in business.&lt;/p&gt;

&lt;p&gt;It gives you HNSW indexes (fast approximate nearest neighbor search), a &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; cosine distance operator, and zero additional infrastructure. Your vector DB is just... your regular DB.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen 2.5, living in my CPU
&lt;/h3&gt;

&lt;p&gt;Here's where things got spicy. Most RAG tutorials assume you'll call GPT-4. But I had no GPU, no cloud credits, and a strong desire to keep everything local.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Qwen2.5-1.5B-Instruct&lt;/strong&gt; — a 1.5 billion parameter model that actually runs on CPU. Not fast, but &lt;em&gt;fast enough&lt;/em&gt;. And the output quality? Genuinely surprising for its size.&lt;/p&gt;

&lt;p&gt;The trick is serving it via &lt;strong&gt;llama-cpp-python&lt;/strong&gt;, which exposes an OpenAI-compatible API at &lt;code&gt;http://127.0.0.1:8001/v1&lt;/code&gt;. The magic of this: my application code uses the exact same &lt;code&gt;OpenAIChatModel&lt;/code&gt; class you'd point at &lt;code&gt;api.openai.com&lt;/code&gt;. It doesn't know — and doesn't care — that the model is running on the same machine, on a CPU, inside a container.&lt;/p&gt;

&lt;p&gt;All it sees is a &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint. The fact that it's a 1.5B model crammed into a Docker container? That's between me and the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  sentence-transformers: tiny but mighty
&lt;/h3&gt;

&lt;p&gt;For embeddings, I picked &lt;strong&gt;all-MiniLM-L6-v2&lt;/strong&gt;. It produces 384-dimensional vectors, runs fast on CPU, and the quality is solid for semantic search. It gets loaded once at startup and cached — no reloading per request:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;

&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-MiniLM-L6-v2&lt;/span&gt;&lt;span class="sh"&gt;"&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;embed_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;get_model&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kubernetes on a laptop? Absolutely.
&lt;/h3&gt;

&lt;p&gt;Running K8s locally sounds excessive until you try it. I used &lt;a href="https://github.com/abiosoft/colima" rel="noopener noreferrer"&gt;Colima&lt;/a&gt; — a lightweight k3s cluster that runs on 2 CPUs and 16GB of RAM. No Docker Desktop, no resource vampires. Just a clean K8s cluster on my Mac.&lt;/p&gt;

&lt;p&gt;The benefit is real: Helm charts for declarative infra, Skaffold for the build-deploy loop, Helm hook Jobs that run migrations and seeding automatically. The same patterns I'd use in production, just at my desk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;Let's walk through the interesting bits. Not every line — just the parts that made me smile when they worked.&lt;/p&gt;

&lt;h3&gt;
  
  
  The database that thinks in vectors
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;documents&lt;/code&gt; table stores each document alongside its 384-dimensional embedding vector. The HNSW index means similarity searches stay fast even as the corpus grows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;       &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt;   &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The search that feels like magic
&lt;/h3&gt;

&lt;p&gt;A stored procedure does the heavy lifting. You throw a vector at it, and it returns the closest matches ranked by similarity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;match_documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;384&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;match_threshold&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;match_count&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="k"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;STABLE&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;match_threshold&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;query_embedding&lt;/span&gt;
    &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="n"&gt;match_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; is pgvector's cosine distance operator. &lt;code&gt;1 - distance&lt;/code&gt; gives us similarity. Clean, fast, entirely in SQL.&lt;/p&gt;

&lt;p&gt;The application calls it through a parameterized SQLAlchemy query — no ORM magic, just raw SQL with safe parameters:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;match_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;match_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&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;list&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="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        SELECT * FROM match_documents(
            CAST(:query_embedding AS vector(384)),
            :match_threshold,
            :match_count
        )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;query_embedding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embedding_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match_threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;match_count&lt;/span&gt;&lt;span class="p"&gt;,&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;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;similarity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity&lt;/span&gt;&lt;span class="sh"&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mappings&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The agent that thinks it's talking to OpenAI
&lt;/h3&gt;

&lt;p&gt;This is my favorite part. Pydantic AI's &lt;code&gt;Agent&lt;/code&gt; class is designed for OpenAI, but llama-cpp-python speaks the same protocol. So we just... point it at localhost:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai.models.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIChatModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic_ai.providers.openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAIProvider&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIChatModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5-1.5b-instruct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;OpenAIProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8001/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not-needed&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="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&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;You are a helpful assistant. Answer questions based on &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the provided context. If the context does not contain &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enough information, say so.&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;The &lt;code&gt;api_key="not-needed"&lt;/code&gt; line makes me happy every time I look at it. Take that, billing page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming: the one that nearly broke me
&lt;/h3&gt;

&lt;p&gt;Server-Sent Events streaming should be straightforward. You yield chunks, the browser displays them. Simple.&lt;/p&gt;

&lt;p&gt;Except Pydantic AI's &lt;code&gt;stream_text()&lt;/code&gt; doesn't return deltas — it returns the &lt;strong&gt;accumulated&lt;/strong&gt; text. Every chunk is the full response so far. If you send that to the browser, it keeps re-rendering the entire message.&lt;/p&gt;

&lt;p&gt;The fix is deceptively simple. Track the previous value and compute the difference:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event: sources&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_text&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;):]&lt;/span&gt;  &lt;span class="c1"&gt;# Just the new stuff
&lt;/span&gt;            &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event: delta&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&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="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&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;Two lines of code that took two hours to figure out. But watching the text flow in, word by word, on the first successful stream — chef's kiss.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ship it: Kubernetes all the way down
&lt;/h2&gt;

&lt;p&gt;Three Helm charts, one &lt;code&gt;skaffold run&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chart&lt;/th&gt;
&lt;th&gt;What's inside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL StatefulSet + pgvector, 5Gi persistent disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FastAPI + llama-cpp running side-by-side, 3Gi model cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag-supabase-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nginx serving the chat UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The slick part: migrations and seeding aren't manual steps. They're Helm hook Jobs that fire automatically on every deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Release.Name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;-migrate-{{ .Release.Revision }}&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post-install,post-upgrade&lt;/span&gt;
    &lt;span class="s"&gt;"helm.sh/hook-weight"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook-delete-policy"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;before-hook-creation&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrate&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.image.repository&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}:{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Values.image.tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uv"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alembic"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upgrade"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;head"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migrate Job fires first (&lt;code&gt;hook-weight: "0"&lt;/code&gt;), then the seed Job (&lt;code&gt;hook-weight: "10"&lt;/code&gt;). The seed Job is smart — it checks if the documents table already has data and skips gracefully. No more &lt;em&gt;"did I already run the seeder?"&lt;/em&gt; anxiety.&lt;/p&gt;

&lt;p&gt;And the LLM model? It downloads a ~1GB GGUF file on first startup and caches it in a PVC at &lt;code&gt;/models&lt;/code&gt;. First deploy takes a few minutes while it downloads. Every deploy after that? Instant. The model survives pod restarts, rebuilds, everything.&lt;/p&gt;




&lt;h2&gt;
  
  
  War stories from the trenches
&lt;/h2&gt;

&lt;p&gt;No project worth building comes without battle scars. Here are the ones that left marks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ghost in the database
&lt;/h3&gt;

&lt;p&gt;After deploying, everything worked perfectly. Come back the next morning, and the app is dead. PostgreSQL connections had silently died during idle periods.&lt;/p&gt;

&lt;p&gt;The fix: &lt;code&gt;pool_pre_ping=True&lt;/code&gt; and &lt;code&gt;pool_recycle=300&lt;/code&gt; on the SQLAlchemy engine. It pings connections before using them and recycles them every 5 minutes. Simple fix, but diagnosing it involved a lot of confused staring at error logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The streaming trap
&lt;/h3&gt;

&lt;p&gt;I already mentioned this, but it deserves its own section. I spent two hours debugging why my streams were "working" but the browser kept re-rendering the entire message every chunk. The culprit: &lt;code&gt;stream_text()&lt;/code&gt; returns cumulative text, not incremental deltas. The &lt;code&gt;chunk[len(prev):]&lt;/code&gt; trick is now burned into my brain forever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency hell, one version at a time
&lt;/h3&gt;

&lt;p&gt;Pydantic AI 0.8.1 depends on &lt;code&gt;opentelemetry._events&lt;/code&gt; — a module that OpenTelemetry removed in version 1.44. The fix? Pin &lt;code&gt;opentelemetry-api&amp;gt;=1.42,&amp;lt;1.43&lt;/code&gt; and move on with your life. Version pinning: not glamorous, but it keeps the lights on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skaffold, Helm hooks, and the great image tag mystery
&lt;/h3&gt;

&lt;p&gt;Skaffold is amazing at building images and deploying Helm charts. But Helm hook Jobs? They reference images through &lt;code&gt;.Values.image.tag&lt;/code&gt;, and Skaffold doesn't inject its build tags into hook templates.&lt;/p&gt;

&lt;p&gt;The workaround: always tag your image as &lt;code&gt;latest&lt;/code&gt; and match it in &lt;code&gt;values.yaml&lt;/code&gt;. Not elegant, but it works. I'll fix it properly someday. (I won't.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The first deploy is always the slowest
&lt;/h3&gt;

&lt;p&gt;That first &lt;code&gt;skaffold run&lt;/code&gt; takes longer because the container is downloading a ~1GB GGUF model. But that's the beauty of Persistent Volumes — the model sticks around. Every subsequent deploy is fast, because the model is already sitting in &lt;code&gt;/models&lt;/code&gt;, warm and waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  So... does it work?
&lt;/h2&gt;

&lt;p&gt;Yes. Beautifully.&lt;/p&gt;

&lt;p&gt;Type a question. Watch the embedding get computed locally. See PostgreSQL find the relevant documents in milliseconds. Read the LLM's response as it streams in, character by character. Check the source citations to see which documents informed the answer.&lt;/p&gt;

&lt;p&gt;And here's the thing that still gets me: &lt;strong&gt;all of it is happening on my laptop.&lt;/strong&gt; No packets leaving my network. No tokens being counted by a billing system somewhere. No vendor lock-in. Just my code, my models, my data.&lt;/p&gt;

&lt;p&gt;The pipeline handles everything end-to-end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Natural language questions → local embedding&lt;/li&gt;
&lt;li&gt;Semantic search → PostgreSQL with pgvector and HNSW&lt;/li&gt;
&lt;li&gt;Retrieved context + question → local LLM via llama-cpp-python&lt;/li&gt;
&lt;li&gt;Streaming response → back to the browser via SSE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One &lt;code&gt;skaffold run&lt;/code&gt; deploys the entire stack. One &lt;code&gt;kubectl port-forward&lt;/code&gt; lets you chat with it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned (and what's next)
&lt;/h2&gt;

&lt;p&gt;This project changed how I think about AI infrastructure. We've gotten so used to reaching for managed services that we forget how much is possible on a single machine. A 1.5B parameter model isn't going to write your novel, but for RAG? For answering questions grounded in your own documents? It's more than enough.&lt;/p&gt;

&lt;p&gt;The whole stack — PostgreSQL, pgvector, FastAPI, sentence-transformers, llama-cpp-python, Kubernetes — fits comfortably in 16GB of RAM. No GPU. No cloud. No API keys taped to the bottom of your keyboard.&lt;/p&gt;

&lt;p&gt;Could it be better? Always. A better model, hybrid search (semantic + keyword), persistent conversation storage, zero-downtime deployments. That's for another weekend.&lt;/p&gt;

&lt;p&gt;For now, I'm just enjoying the quiet satisfaction of asking my local LLM a question and watching it answer — powered by nothing but my CPU and a few hundred lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/nf1s/rag-supabase" rel="noopener noreferrer"&gt;github.com/nf1s/rag-supabase&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you build something similar, tag me on Dev.to. I'd love to see what you cook up without a credit card. And if you run into the same streaming bug — &lt;code&gt;chunk[len(prev):]&lt;/code&gt;. You're welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>rag</category>
      <category>llm</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
