<?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: opensource</title>
    <description>The latest articles tagged 'opensource' on DEV Community.</description>
    <link>https://dev.to/t/opensource</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/opensource"/>
    <language>en</language>
    <item>
      <title>🚀 Building Spring by Example: Completing the Spring Transactions Module</title>
      <dc:creator>Mujuzi Moses </dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:54:17 +0000</pubDate>
      <link>https://dev.to/iammujuzimoses/building-spring-by-example-completing-the-spring-transactions-module-3f8c</link>
      <guid>https://dev.to/iammujuzimoses/building-spring-by-example-completing-the-spring-transactions-module-3f8c</guid>
      <description>&lt;p&gt;When I started learning Spring, I noticed something about many of the tutorials I found.&lt;/p&gt;

&lt;p&gt;They often showed how to use a feature, but not always why it worked or when you should choose one approach over another.&lt;/p&gt;

&lt;p&gt;I learn best by building things and documenting what I discover, so I started a project called &lt;strong&gt;spring-by-example&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of building one large application, I’m creating a collection of small, focused examples where each example explores a specific Spring concept with clear explanations, tests, and documentation.&lt;/p&gt;

&lt;p&gt;I’m happy to say that I’ve now completed &lt;strong&gt;Module 8 — Transactions&lt;/strong&gt;. 🎉&lt;/p&gt;

&lt;p&gt;The module covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Transactional&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Transaction Propagation&lt;/li&gt;
&lt;li&gt;Isolation Levels&lt;/li&gt;
&lt;li&gt;Rollback Rules&lt;/li&gt;
&lt;li&gt;Programmatic Transactions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransactionProxyFactoryBean&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most interesting parts of this module was understanding what actually happens when Spring manages a transaction.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;@Transactional&lt;/code&gt; makes it possible to declaratively define transaction boundaries without having to manually begin, commit, or roll back transactions in application code.&lt;/p&gt;

&lt;p&gt;Transaction propagation introduced another important concept: what should happen when a transactional method calls another transactional method?&lt;/p&gt;

&lt;p&gt;I explored propagation behaviors such as &lt;code&gt;REQUIRED&lt;/code&gt; and &lt;code&gt;REQUIRES_NEW&lt;/code&gt;, and how they affect whether operations participate in the same transaction or run in an independent transaction.&lt;/p&gt;

&lt;p&gt;Isolation levels then showed another side of transaction management: how concurrent transactions interact with data and what kinds of visibility and consistency guarantees they provide.&lt;/p&gt;

&lt;p&gt;Rollback rules were another useful area to explore. A transaction does not necessarily roll back for every exception automatically, so understanding how Spring determines whether a transaction should be rolled back is important when designing transactional services.&lt;/p&gt;

&lt;p&gt;I also explored programmatic transactions, which provide more explicit control over transaction boundaries when declarative transaction management isn't the right fit.&lt;/p&gt;

&lt;p&gt;Finally, I looked at &lt;code&gt;TransactionProxyFactoryBean&lt;/code&gt;, an older Spring approach to applying transaction management through proxies. It was especially useful for understanding how Spring's transaction infrastructure evolved and how it connects to the proxy concepts explored in the previous AOP module.&lt;/p&gt;

&lt;p&gt;The goal isn’t just to collect Spring annotations and APIs. I’m trying to understand how the Spring container works, how its features fit together, and why these features exist, one concept at a time.&lt;/p&gt;

&lt;p&gt;The project is open source, and I’m continuing to build it as I learn:&lt;/p&gt;

&lt;p&gt;🔗 GitHub: &lt;a href="https://github.com/IamMujuziMoses/spring-by-example" rel="noopener noreferrer"&gt;githu.b/sbe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 GitLab: &lt;a href="https://gitlab.com/IamMujuziMoses/spring-by-example" rel="noopener noreferrer"&gt;gitla.b/sbe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s still a long way to go, but completing another module feels like a good milestone. 🚀&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next up: Spring JDBC&lt;/strong&gt; — continuing to explore how Spring works with databases and how its abstractions simplify database access.&lt;/p&gt;

&lt;p&gt;If you’ve worked with Spring before, what part of Spring's transaction management do you think developers commonly misunderstand?&lt;/p&gt;

&lt;h1&gt;
  
  
  java #springframework #opensource #github #gitlab #learninginpublic #backend #softwareengineering #spring #transactions
&lt;/h1&gt;

</description>
      <category>opensource</category>
      <category>java</category>
      <category>spring</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Reddit’s ChatGPT Citation Drop Isn’t Fully Explained</title>
      <dc:creator>eMarketters - Digital Marketing Company in Lucknow</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:44:27 +0000</pubDate>
      <link>https://dev.to/emarketters_5e793dfb26ee2/why-reddits-chatgpt-citation-drop-isnt-fully-explained-3iek</link>
      <guid>https://dev.to/emarketters_5e793dfb26ee2/why-reddits-chatgpt-citation-drop-isnt-fully-explained-3iek</guid>
      <description>&lt;p&gt;Reddit has recently become far less visible in ChatGPT Search citations, creating an important discussion among SEO and Generative Engine Optimization (GEO) professionals.&lt;/p&gt;

&lt;p&gt;According to Promptwatch’s citation tracking data, Reddit accounted for an average of 3.83% of ChatGPT Search citations between July 18 and August 7, 2026. By August 14, that figure had dropped below 1%, reaching an average of just 0.52% between August 14 and August 17.&lt;/p&gt;

&lt;p&gt;This represents an 86.4% relative decline.&lt;/p&gt;

&lt;p&gt;The timing is particularly interesting because ChatGPT’s web-search behaviour also appeared to change around August 8. However, it is important not to jump to conclusions.&lt;/p&gt;

&lt;p&gt;While the data clearly shows that Reddit’s citation share declined and ChatGPT’s search behaviour changed, the exact reason behind the drop has not yet been conclusively proven.&lt;/p&gt;

&lt;p&gt;What Happened to Reddit’s ChatGPT Citations?&lt;br&gt;
Before August 8, Reddit’s citation visibility in ChatGPT Search appeared relatively stable. It averaged around 3.83% during the measured period.&lt;/p&gt;

&lt;p&gt;The first noticeable decline began around August 8, when Reddit’s share moved from the high 3% range into the mid-2% range. The larger drop occurred around August 14, when Reddit’s citation share fell below 1%.&lt;/p&gt;

&lt;p&gt;Between August 14 and August 17, Reddit averaged just 0.52% of ChatGPT Search citations.&lt;/p&gt;

&lt;p&gt;This does not necessarily mean that ChatGPT completely stopped using Reddit. Instead, it suggests that Reddit became significantly less likely to appear as a visible citation in ChatGPT Search results.&lt;/p&gt;

&lt;p&gt;That distinction is important because content can potentially influence an AI-generated response without always appearing as a visible citation.&lt;/p&gt;

&lt;p&gt;Did ChatGPT Change How It Searches the Web?&lt;br&gt;
One of the most interesting developments occurred on August 8, 2026.&lt;/p&gt;

&lt;p&gt;Promptwatch observed a major change in ChatGPT’s query fanout behaviour. Query fanout refers to the additional searches an AI system performs behind the scenes to gather information before generating an answer.&lt;/p&gt;

&lt;p&gt;The share of fanout queries using the site: search operator reportedly increased from approximately 0.37% to 16.8%. At the same time, the average number of searches performed per response increased from approximately 1.08 to 1.83.&lt;/p&gt;

&lt;p&gt;A site: search limits results to a specific website or domain.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;site:example.com SEO services&lt;/p&gt;

&lt;p&gt;Instead of searching across the entire web, the search is focused specifically on that domain.&lt;/p&gt;

&lt;p&gt;This change could be significant for GEO and AI visibility because domain-specific searches may create different opportunities for websites to be discovered and cited.&lt;/p&gt;

&lt;p&gt;Could This Change Explain Reddit’s Decline?&lt;br&gt;
Possibly—but it has not been proven.&lt;/p&gt;

&lt;p&gt;Reddit contains millions of discussions, reviews, personal experiences, opinions, recommendations, and niche conversations. However, when an AI system performs a domain-specific search, it may be more likely to investigate sources such as:&lt;/p&gt;

&lt;p&gt;Official company websites&lt;br&gt;
Government websites&lt;br&gt;
Documentation&lt;br&gt;
Institutional sources&lt;br&gt;
Product websites&lt;br&gt;
Brand-owned content&lt;br&gt;
Authoritative reference pages&lt;br&gt;
Because of this, the increase in domain-specific searching provides a plausible explanation for Reddit’s sudden decline in visible ChatGPT citations.&lt;/p&gt;

&lt;p&gt;However, there is an important timing issue.&lt;/p&gt;

&lt;p&gt;ChatGPT’s search behaviour appeared to change around August 8, while Reddit’s largest citation decline occurred around August 14.&lt;/p&gt;

&lt;p&gt;This gap means that we cannot confidently say the August 8 change directly caused the entire Reddit citation collapse.&lt;/p&gt;

&lt;p&gt;The two events are clearly related in timing, but correlation does not automatically prove causation.&lt;/p&gt;

&lt;p&gt;Is ChatGPT Penalizing Reddit?&lt;br&gt;
At the moment, there is not enough evidence to say that ChatGPT has specifically penalized Reddit.&lt;/p&gt;

&lt;p&gt;The available data appears to point more toward a broader change in ChatGPT’s retrieval and search behaviour rather than a direct action against one particular platform.&lt;/p&gt;

&lt;p&gt;Interestingly, Reddit’s visibility did not decline equally across every AI search platform.&lt;/p&gt;

&lt;p&gt;Promptwatch reported a much smaller decline in Reddit citation visibility within Google’s AI products:&lt;/p&gt;

&lt;p&gt;Google AI Overviews: Approximately 11.3% relative decline&lt;br&gt;
Google AI Mode: Approximately 30.5% relative decline&lt;br&gt;
ChatGPT Search: Approximately 86.4% relative decline&lt;br&gt;
This difference suggests that the major disruption may be more closely connected to how ChatGPT retrieves and selects information rather than Reddit suddenly becoming less useful across all AI search systems.&lt;/p&gt;

&lt;p&gt;Does This Mean Reddit Is No Longer Useful for GEO?&lt;br&gt;
No.&lt;/p&gt;

&lt;p&gt;Businesses and marketers should not abandon Reddit based on a short period of citation data.&lt;/p&gt;

&lt;p&gt;Reddit continues to provide valuable user-generated content, including real experiences, reviews, discussions, questions, opinions, and community insights.&lt;/p&gt;

&lt;p&gt;The real lesson is that no single platform should be treated as a guaranteed source of AI visibility.&lt;/p&gt;

&lt;p&gt;A strong GEO strategy should build a broader digital presence across multiple sources, including:&lt;/p&gt;

&lt;p&gt;Your own website&lt;br&gt;
Trusted industry publications&lt;br&gt;
Authoritative third-party websites&lt;br&gt;
Relevant online communities&lt;br&gt;
News platforms&lt;br&gt;
Expert interviews&lt;br&gt;
Business directories&lt;br&gt;
Professional platforms&lt;br&gt;
Product and service pages&lt;br&gt;
Reliable reference sources&lt;br&gt;
The objective should not simply be to get one Reddit thread cited by ChatGPT.&lt;/p&gt;

&lt;p&gt;Instead, businesses should focus on creating a strong, trustworthy, and diversified presence across the web.&lt;/p&gt;

&lt;p&gt;What Does This Mean for SEO and GEO?&lt;br&gt;
The Reddit citation decline demonstrates how quickly AI search visibility can change.&lt;/p&gt;

&lt;p&gt;Traditional SEO professionals are already familiar with algorithm updates and changes in search rankings. GEO introduces additional variables because AI systems can modify several parts of their retrieval process.&lt;/p&gt;

&lt;p&gt;These may include:&lt;/p&gt;

&lt;p&gt;Search queries&lt;br&gt;
Query fanout&lt;br&gt;
Source selection&lt;br&gt;
Retrieval depth&lt;br&gt;
Domain targeting&lt;br&gt;
Citation behaviour&lt;br&gt;
Number of searches&lt;br&gt;
Number of sources used&lt;br&gt;
Even a small change in one of these areas can affect which websites appear in AI-generated answers.&lt;/p&gt;

&lt;p&gt;This means a strategy that performs well today may not produce identical results in the future.&lt;/p&gt;

&lt;p&gt;What Should SEO Professionals Do Now?&lt;br&gt;
Rather than focusing only on Reddit’s overall decline, marketers should monitor their own AI search visibility.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track Important Prompts
Create a consistent list of prompts related to your business, products, or services.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, an agency could monitor prompts such as:&lt;/p&gt;

&lt;p&gt;What are the best digital marketing companies in Lucknow?&lt;br&gt;
Which SEO agencies in Lucknow are recommended?&lt;br&gt;
Who provides the best SEO services in Lucknow?&lt;br&gt;
Best website development companies in Lucknow&lt;br&gt;
Run these searches regularly across different AI platforms and monitor changes over time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Which Sources Are Being Cited
Do not only track whether your brand appears in the answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also monitor:&lt;/p&gt;

&lt;p&gt;Which websites are cited&lt;br&gt;
Which pages receive citations&lt;br&gt;
Which competitors appear&lt;br&gt;
Whether your own website is cited&lt;br&gt;
How frequently your brand appears&lt;br&gt;
Which third-party sources influence visibility&lt;br&gt;
This can provide a clearer picture of your overall GEO performance.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Improve Website Crawlability
If AI systems are increasingly performing domain-specific searches, your own website may become even more important as a direct source of information.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Make sure your important pages are:&lt;/p&gt;

&lt;p&gt;Crawlable&lt;br&gt;
Indexable&lt;br&gt;
Internally linked&lt;br&gt;
Clearly structured&lt;br&gt;
Regularly updated&lt;br&gt;
Relevant to user queries&lt;br&gt;
Supported with useful and original content&lt;br&gt;
A technically weak website may limit what AI search systems can discover.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Track AI Platforms Separately
Do not combine visibility from ChatGPT, Google AI Overviews, and Google AI Mode into one general AI visibility score.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each platform may use different retrieval methods and source-selection processes.&lt;/p&gt;

&lt;p&gt;A decline in one platform does not necessarily mean the same decline will happen everywhere.&lt;/p&gt;

&lt;p&gt;The Bigger Lesson for GEO&lt;br&gt;
It is still too early to determine whether Reddit’s reduced ChatGPT citation share is permanent.&lt;/p&gt;

&lt;p&gt;AI search systems can change quickly. Reddit could regain visibility if ChatGPT changes its search or retrieval behaviour again.&lt;/p&gt;

&lt;p&gt;For now, the data shows a significant decline in Reddit’s visible ChatGPT citations during August 2026. The exact cause, however, remains uncertain.&lt;/p&gt;

&lt;p&gt;The biggest lesson for SEO professionals is simple:&lt;/p&gt;

&lt;p&gt;Never build your AI visibility strategy around a single platform or source.&lt;/p&gt;

&lt;p&gt;Invest in your own website, publish useful original content, strengthen technical SEO, earn authoritative third-party mentions, participate in relevant communities, and continuously monitor how your brand appears across AI search platforms.&lt;/p&gt;

&lt;p&gt;Read Full Blog Here&lt;br&gt;
Read the full blog here: [&lt;a href="https://emarketters.com/reddit-chatgpt-citation-drop-2026/" rel="noopener noreferrer"&gt;https://emarketters.com/reddit-chatgpt-citation-drop-2026/&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;As AI search continues to evolve, diversification, adaptability, and continuous monitoring will become increasingly important for long-term GEO success.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built a Free Watermark-Free Xiaohongshu (RedNote) Downloader — and What I Learned About Anti-Scraping</title>
      <dc:creator>wangxiaolou</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:38:10 +0000</pubDate>
      <link>https://dev.to/wangxiaolou/how-i-built-a-free-watermark-free-xiaohongshu-rednote-downloader-and-what-i-learned-about-47oa</link>
      <guid>https://dev.to/wangxiaolou/how-i-built-a-free-watermark-free-xiaohongshu-rednote-downloader-and-what-i-learned-about-47oa</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Xiaohongshu (also known as RED or RedNote) is China's fastest-growing lifestyle platform — 300M+ monthly active users sharing beauty tutorials, product reviews, and travel content. But if you've ever tried to save a video from it, you know the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen recording = terrible quality + watermark visible&lt;/li&gt;
&lt;li&gt;Third-party APKs = security nightmare&lt;/li&gt;
&lt;li&gt;Most "downloaders" online = sketchy ads, no preview, or straight-up broken&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;a href="https://xiaohongshudownloader.com" rel="noopener noreferrer"&gt;xiaohongshudownloader.com&lt;/a&gt; to fix this: paste a share link → see a preview (title, author, thumbnail) → download the clean MP4 or original-quality image. No sign-up, no app install, works on any device.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Resolving Share Links
&lt;/h3&gt;

&lt;p&gt;Xiaohongshu's share system generates short links (&lt;code&gt;xhslink.com/xxx&lt;/code&gt; or &lt;code&gt;xhslink.cn/xxx&lt;/code&gt;) that redirect to the actual note. But they come in multiple flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short links (need redirect resolution)&lt;/li&gt;
&lt;li&gt;Full share text (title + emoji + URL — users paste the whole thing)&lt;/li&gt;
&lt;li&gt;Explore URLs with &lt;code&gt;xsec_token&lt;/code&gt; parameters&lt;/li&gt;
&lt;li&gt;International RedNote URLs (rednote.com)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My parser handles all four formats. The tricky part: &lt;code&gt;xsec_token&lt;/code&gt; expires, so cached tokens break. I had to implement fresh-token fetching with fallback logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Bypassing Watermarks
&lt;/h3&gt;

&lt;p&gt;Here's the insight most downloaders miss: Xiaohongshu's watermark isn't burned into the original file. It's applied at the CDN layer when served through certain endpoints. The original MP4 on the CDN source is clean.&lt;/p&gt;

&lt;p&gt;My approach: fetch the note's metadata → extract the direct CDN source URL → serve that to the user. No re-encoding, no quality loss, no watermark overlay.&lt;/p&gt;

&lt;p&gt;(This is also why some downloads DO have watermarks — if the note is private or the CDN endpoint requires the watermarked version, I can't bypass it. I'm honest about this on the site.)&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Anti-Scraping Countermeasures
&lt;/h3&gt;

&lt;p&gt;Xiaohongshu (understandably) doesn't want mass scraping. Their defenses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signature generation:&lt;/strong&gt; Each API request needs a valid signature (computed client-side, changes regularly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting per IP:&lt;/strong&gt; Too many requests = temporary blocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie/session validation:&lt;/strong&gt; Requests without proper cookies get rejected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-link expiration:&lt;/strong&gt; Tokens in URLs expire after a window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My countermeasures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;24-hour response caching:&lt;/strong&gt; Once I resolve a note, I cache the result. Same link = instant response from cache, no repeat hitting their API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request throttling:&lt;/strong&gt; Per-IP rate limits on my end prevent abuse (and keep my IP pool healthy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful degradation:&lt;/strong&gt; When a link fails (private note, expired token, timeout), I return a specific error message telling the user WHY it failed and how to fix it — instead of a generic "something went wrong."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Multi-Format Support
&lt;/h3&gt;

&lt;p&gt;Videos are straightforward (MP4). But Xiaohongshu is heavily image-based:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single photos:&lt;/strong&gt; WebP or JPG from the CDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Albums/carousels:&lt;/strong&gt; Multiple images in order, with a "download all as ZIP" option&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Photos:&lt;/strong&gt; Not supported (honestly disclosed — these require special handling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added batch mode: paste up to 8 links at once, get all results in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture (Simplified)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User pastes link
    ↓
[Link Parser] → detects format, resolves redirects
    ↓
[Cache Check] → hit? return cached result
    ↓ miss
[Metadata Fetcher] → signature, cookies, API call
    ↓
[Media Extractor] → CDN source URLs (video/images)
    ↓
[Preview Card] → title, author, thumbnail, format
    ↓
[Download] → direct CDN URL or proxied stream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole thing is a single Next.js app. No database — just an in-memory + Redis cache layer. Total infra cost: under $20/month at current traffic (~15k visits/month).&lt;/p&gt;

&lt;h2&gt;
  
  
  What Surprised Me: The Indonesian Market
&lt;/h2&gt;

&lt;p&gt;I built this tool expecting Chinese diaspora and Chinese-language learners to be my audience. Wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;63% of my traffic comes from Indonesia.&lt;/strong&gt; Indonesian creators and e-commerce resellers use Xiaohongshu as a product-sourcing and content-inspiration platform. They search in English and Indonesian for "download xiaohongshu video" — and found my tool.&lt;/p&gt;

&lt;p&gt;This is a live lesson in building for a global audience: you don't always get the users you design for. Sometimes the market finds you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;If you want to test it: &lt;a href="https://xiaohongshudownloader.com" rel="noopener noreferrer"&gt;xiaohongshudownloader.com&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paste any public Xiaohongshu/RedNote share link&lt;/li&gt;
&lt;li&gt;Works for videos, photos, and albums&lt;/li&gt;
&lt;li&gt;Free, no sign-up, works on mobile&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Building in public. Follow my progress or ask technical questions — happy to share more details about the scraping architecture in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>FlottaMezzi: a free fleet management app with geolocation, maintenance deadlines and cost rollups</title>
      <dc:creator>Wuic Framework</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:35:20 +0000</pubDate>
      <link>https://dev.to/wuicframework/flottamezzi-a-free-fleet-management-app-with-geolocation-maintenance-deadlines-and-cost-rollups-2d8d</link>
      <guid>https://dev.to/wuicframework/flottamezzi-a-free-fleet-management-app-with-geolocation-maintenance-deadlines-and-cost-rollups-2d8d</guid>
      <description>&lt;p&gt;&lt;code&gt;FlottaMezzi&lt;/code&gt; is the third and final app in our free distribution lineup. It targets the problem space that mid-size logistics / construction / service companies handle in Excel until someone reaches their limit: tracking what vehicles the company owns, what's due on each of them, where they are right now if they're moving, and what each one is costing per month.&lt;/p&gt;

&lt;p&gt;This post covers what's in the box, who tends to install this kind of thing, and the same licensing rule that applies to the other two free apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ships in the free distribution
&lt;/h2&gt;

&lt;p&gt;Download is &lt;code&gt;FlottaMezzi-iis-v1.5.0-with-dbs.zip&lt;/code&gt; on the &lt;a href="https://wuic-framework.com/downloads#free-apps" rel="noopener noreferrer"&gt;Downloads page&lt;/a&gt;. Inside:&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry &amp;amp; ownership
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vehicles (mezzi)&lt;/strong&gt; — plate, chassis number, make/model, year, fuel type, current odometer, vehicle type and status (both lookup-driven, so you can add your own classes), assigned driver, and last known GPS position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drivers (conducenti)&lt;/strong&gt; — personal data, license number, license category, license expiry date, contacts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance (manutenzioni)&lt;/strong&gt; — date, odometer at service, type, cost, workshop, invoice reference&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuelings (rifornimenti)&lt;/strong&gt; — litres, total cost, price per litre, odometer reading; a DB trigger propagates the odometer reading back to the vehicle, so &lt;code&gt;km_attuali&lt;/code&gt; stays current without a separate data-entry step&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insurance contracts (contratti assicurativi)&lt;/strong&gt; — company, policy number, start/expiry, annual cost, coverage type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspections (revisioni)&lt;/strong&gt; — date, outcome, next due date, inspection centre, cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incidents (sinistri)&lt;/strong&gt; — date, driver, counterparty, estimated cost, claim status; a trigger flips the vehicle status when an incident comes in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is a metadata-driven WUIC route, so each list ships with search, filters, Excel export and an edit dialog for free — and adding a column to the SQL table makes it appear in the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deadline tracking
&lt;/h3&gt;

&lt;p&gt;The piece that triggers an ROI conversation: driver license expiry, insurance expiry and inspection due dates all live in the data model, and a scheduled job (&lt;code&gt;flottamezzi_check_scadenze&lt;/code&gt;, a row in the framework's &lt;code&gt;scheduler&lt;/code&gt; table, daily at 07:00) scans for anything expiring in the next 30 days — plus anything already expired — and reports the counts. The home dashboard has a &lt;strong&gt;deadline aging&lt;/strong&gt; widget built on the same data, so "what's about to bite us" is the first thing a fleet manager sees.&lt;/p&gt;

&lt;p&gt;Out of the box the job produces the scan summary and the dashboard does the surfacing; wiring the scan result to outbound email or the in-app notification bell is a documented extension point (the scheduler row already carries an exception-notification address).&lt;/p&gt;

&lt;h3&gt;
  
  
  Geolocation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /api/Geolocation/UpdatePosition&lt;/code&gt; accepts position updates from anything that can authenticate and POST a JSON payload:&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;"mezzo_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"latitudine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;45.4642&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"longitudine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.1900&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 endpoint requires an authenticated session with the &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;gestore_flotta&lt;/code&gt; or &lt;code&gt;autista&lt;/code&gt; role — so a driver's phone, a scripted tracker bridge, or a back-office import can all feed it, but a random unauthenticated device can't. It updates the vehicle's last known position, and a &lt;code&gt;mezzi_posizioni&lt;/code&gt; history table (timestamp, coordinates, speed) accumulates the trail, with a per-day route view used by the map for &lt;strong&gt;route playback&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The UI is the framework's map archetype — Google Maps with marker clustering, custom marker colouring per vehicle, and polyline rendering for routes. You supply your own key in &lt;code&gt;appsettings.json&lt;/code&gt; under &lt;code&gt;GoogleMaps:ApiKey&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwuic-framework.com%2Fassets%2Fwuic-framework-docs%2Fscreenshots%2Fmap-list__map-marker__desktop.gif" 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%2Fwuic-framework.com%2Fassets%2Fwuic-framework-docs%2Fscreenshots%2Fmap-list__map-marker__desktop.gif" alt="Map list — live markers with clustering, click a vehicle for its info window" width="480" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We do NOT ship a hardware integration in the free distribution — you bring your own device or bridge that POSTs to the endpoint. If you don't have a tracker yet, a &lt;code&gt;curl&lt;/code&gt; loop with a logged-in session is enough to see the map move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost roll-ups
&lt;/h3&gt;

&lt;p&gt;Costs flow in from three sources — maintenance, fuel, incidents — and roll up in three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ReportCostiMezzo&lt;/code&gt;&lt;/strong&gt; — per-vehicle cost report over a selectable year range, broken down by maintenance / fuel / claims, sorted by total (the "which vehicle is bleeding us" view)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly cost view&lt;/strong&gt; (&lt;code&gt;vw_costi_per_mese&lt;/code&gt;) — feeding the dashboard's monthly trend and &lt;strong&gt;cost forecast&lt;/strong&gt; widgets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top vehicles dashboard&lt;/strong&gt; — the most expensive vehicles at a glance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A second scheduled job (&lt;code&gt;flottamezzi_aggrega_costi&lt;/code&gt;, daily at 02:00) keeps the aggregates fresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard
&lt;/h3&gt;

&lt;p&gt;The home dashboard ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deadline aging (what expires in the next 30 days, what's already expired)&lt;/li&gt;
&lt;li&gt;Monthly cost trend + forecast&lt;/li&gt;
&lt;li&gt;Top vehicles by cost&lt;/li&gt;
&lt;li&gt;Live map with current positions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who installs this kind of app
&lt;/h2&gt;

&lt;p&gt;It pays off if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have &lt;strong&gt;15+ vehicles&lt;/strong&gt; or plated equipment — below that threshold, Excel plus a shared calendar covers it&lt;/li&gt;
&lt;li&gt;You run &lt;strong&gt;multiple sites or mobile crews&lt;/strong&gt; — knowing where every vehicle is in real time kills the "who took the van and where did they leave it?" phone calls&lt;/li&gt;
&lt;li&gt;You operate in a &lt;strong&gt;regulated sector&lt;/strong&gt; (heavy goods transport, waste) where document deadlines get audited&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;stop paying per-vehicle-per-month&lt;/strong&gt; to a vertical SaaS — at 50 vehicles even a few euros per vehicle per month adds up to thousands per year, and this app runs free on a server you already have&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does NOT pay off if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have 5 vehicles or fewer&lt;/li&gt;
&lt;li&gt;You want white-glove SaaS with zero infrastructure to manage — in that case buy a cloud service&lt;/li&gt;
&lt;li&gt;You need native fuel-card integrations (DKV, Eni, UTA) with automatic transaction import — the free app tracks refuelings as records you enter or feed via the API; native card integrations are the kind of extension that lives on the commercial side&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Same flow as the other free apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download &lt;code&gt;FlottaMezzi-iis-v1.5.0-with-dbs.zip&lt;/code&gt; from &lt;a href="https://wuic-framework.com/downloads#free-apps" rel="noopener noreferrer"&gt;Downloads&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Unzip into &lt;code&gt;C:\inetpub\wwwroot\Flotta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restore the two &lt;code&gt;.bak&lt;/code&gt; files shipped in the &lt;code&gt;db\&lt;/code&gt; folder (&lt;code&gt;data.bak&lt;/code&gt; + &lt;code&gt;metadata.bak&lt;/code&gt;) — SQL Server 2017 or later, Express is enough; the bundled &lt;code&gt;INSTALL.md&lt;/code&gt; has the exact &lt;code&gt;RESTORE DATABASE&lt;/code&gt; statements&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;appsettings.json&lt;/code&gt; for the two connection strings, plus your &lt;code&gt;GoogleMaps:ApiKey&lt;/code&gt; for the map&lt;/li&gt;
&lt;li&gt;IIS site, app pool set to No Managed Code, browse, log in as &lt;code&gt;admin_test / Test123!&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the geolocation feed: authenticate (any user with the &lt;code&gt;autista&lt;/code&gt; or &lt;code&gt;gestore_flotta&lt;/code&gt; role), then POST to &lt;code&gt;/api/Geolocation/UpdatePosition&lt;/code&gt; with the session cookie. No tracker hardware yet? Simulate with &lt;code&gt;curl&lt;/code&gt; and a loop to test the map.&lt;/p&gt;

&lt;h2&gt;
  
  
  License rule
&lt;/h2&gt;

&lt;p&gt;Identical to the other two free apps:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;FlottaMezzi is free as-shipped. If you rebuild the app from source you need a WUIC license.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The free ZIP gives you the ready-to-run binary (&lt;code&gt;FlottaMezzi.dll&lt;/code&gt;) with the embedded host-binding &lt;code&gt;.lic&lt;/code&gt; that authorizes the framework runtime. The source ZIP gives you the app's C# — but recompiling it locally produces a binary with a different identity (different strong-name PKT, no embedded &lt;code&gt;WUIC.HostBindingLicense&lt;/code&gt; resource — we don't ship the .snk or the RSA private key that signs the .lic), so the framework stops recognizing the host as authorized and falls back to the standard fingerprint check.&lt;/p&gt;

&lt;p&gt;You can extend FlottaMezzi without recompiling the binary: add metadata via SQL, add custom Angular components, add Stimulsoft reports, add scheduled jobs in the &lt;code&gt;scheduler&lt;/code&gt; table, change the Maps key or connection strings in &lt;code&gt;appsettings.json&lt;/code&gt;. Adding a new controller (say, to integrate DKV fuel-card imports) means changing the app's C# and recompiling &lt;code&gt;FlottaMezzi.dll&lt;/code&gt;, so that crosses the license line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt;: &lt;a href="https://wuic-framework.com/downloads#free-apps" rel="noopener noreferrer"&gt;Downloads → Free apps → FlottaMezzi&lt;/a&gt; — current release is &lt;strong&gt;v1.5.0&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try WUIC first&lt;/strong&gt;: the framework underneath has a live sandbox at &lt;a href="https://demo.wuic-framework.com/" rel="noopener noreferrer"&gt;demo.wuic-framework.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need fuel-card integrations or white-label trackers?&lt;/strong&gt; See &lt;a href="https://wuic-framework.com/pricing" rel="noopener noreferrer"&gt;Pricing&lt;/a&gt; — the Developer tier unlocks the framework source and the right to ship recompiled FlottaMezzi binaries inside your products&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>iot</category>
      <category>geolocation</category>
      <category>opensource</category>
      <category>wuic</category>
    </item>
    <item>
      <title>I Turned a $10 USB Drive Into a Portable, Offline AI Assistant — Here's How You Can Too</title>
      <dc:creator>Aman Kumar Dewangan</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:34:16 +0000</pubDate>
      <link>https://dev.to/amandewatnitrr/i-turned-a-10-usb-drive-into-a-portable-offline-ai-assistant-heres-how-you-can-too-573o</link>
      <guid>https://dev.to/amandewatnitrr/i-turned-a-10-usb-drive-into-a-portable-offline-ai-assistant-heres-how-you-can-too-573o</guid>
      <description>&lt;p&gt;&lt;em&gt;No cloud, no subscription, no internet required — how to run a quantized LLM entirely from a USB drive using llamafile.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've ever wanted your own private AI assistant — one that runs entirely on your machine, never sends a single token to a third-party server, and works on a plane with no wifi — this guide walks through exactly how to build one. By the end, you'll have a self-contained AI environment that lives on a USB drive and boots on any Windows laptop in under a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build This
&lt;/h2&gt;

&lt;p&gt;Most people assume running a capable large language model requires a GPU rig, a cloud subscription, or at minimum a beefy always-on machine. That's no longer true. Thanks to model quantization and single-binary inference engines, a 7-8B parameter model can now run comfortably on consumer laptop CPUs, packaged into a single portable executable.&lt;/p&gt;

&lt;p&gt;The benefits of a pendrive-based setup specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data sovereignty&lt;/strong&gt; — nothing you type ever leaves the local machine or network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero marginal cost&lt;/strong&gt; — no per-token API billing, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True portability&lt;/strong&gt; — plug into any Windows machine, get the same AI, no installation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline capable&lt;/strong&gt; — once the files are loaded, no internet connection is needed at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A genuine systems lesson&lt;/strong&gt; — you'll understand how LLM inference, quantization, and local networking actually fit together, instead of treating it as a black box behind an API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You'll Need
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A USB drive (16GB+, 32GB recommended)&lt;/td&gt;
&lt;td&gt;Storage for the engine and model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/Mozilla-Ocho/llamafile" rel="noopener noreferrer"&gt;llamafile&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Single-executable LLM inference engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A GGUF-format quantized model&lt;/td&gt;
&lt;td&gt;The actual language model weights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A Windows/macOS/Linux laptop&lt;/td&gt;
&lt;td&gt;Host machine to run it on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(Optional) A local chat UI like llama-ui&lt;/td&gt;
&lt;td&gt;Nicer front-end than the raw CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On model choice: pick any instruction-tuned open-weight model in GGUF format from Hugging Face — Qwen2.5/3, Llama 3, Mistral, and Gemma all have well-supported quantized releases. A &lt;code&gt;Q4_K_M&lt;/code&gt; quantization is the sweet spot between size and coherence for CPU inference; it roughly halves the model's footprint versus full precision with minimal quality loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set Up the Folder Structure on Your Drive
&lt;/h2&gt;

&lt;p&gt;Plug in your pendrive and lay out a structure 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;E:\PortableAI\
├── bin\
│   └── llamafile\
│       └── llamafile.exe
├── models\
│   └── your-model-Q4_K_M.gguf
└── run-portable-ai.bat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping the engine and model in fixed relative paths means the launch script never has to hardcode a drive letter — it works whether the pendrive mounts as &lt;code&gt;E:&lt;/code&gt;, &lt;code&gt;F:&lt;/code&gt;, or anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Download llamafile
&lt;/h2&gt;

&lt;p&gt;Grab the latest &lt;code&gt;llamafile.exe&lt;/code&gt; release from the &lt;a href="https://github.com/Mozilla-Ocho/llamafile" rel="noopener noreferrer"&gt;official llamafile repo&lt;/a&gt;. It's a single binary — no installer, no dependencies. Drop it into &lt;code&gt;bin\llamafile\&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;llamafile bundles a llama.cpp-based inference engine and a lightweight web server into one executable, which is exactly what makes this portable: one file runs the model &lt;em&gt;and&lt;/em&gt; serves a chat interface over HTTP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Download a Quantized Model
&lt;/h2&gt;

&lt;p&gt;Head to Hugging Face and search for GGUF builds of your model of choice (e.g., "Qwen2.5-7B-Instruct-GGUF"). Download the &lt;code&gt;Q4_K_M&lt;/code&gt; variant — it typically lands in the 4–5GB range for a 7-8B model, which fits comfortably on any USB 3.0 drive alongside the engine.&lt;/p&gt;

&lt;p&gt;Place the &lt;code&gt;.gguf&lt;/code&gt; file in &lt;code&gt;models\&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Write the Launch Script
&lt;/h2&gt;

&lt;p&gt;This is the part that makes the whole thing feel like a real product instead of a CLI toy. Create &lt;code&gt;run-portable-ai.bat&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;@echo &lt;span class="na"&gt;off&lt;/span&gt;
&lt;span class="nb"&gt;setlocal&lt;/span&gt; &lt;span class="na"&gt;enabledelayedexpansion&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"DIR=&lt;/span&gt;&lt;span class="vm"&gt;%~dp0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"MODEL=&lt;/span&gt;&lt;span class="nv"&gt;%DIR%&lt;/span&gt;&lt;span class="s2"&gt;models\your-model-Q4_K_M.gguf"&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"BIN=&lt;/span&gt;&lt;span class="nv"&gt;%DIR%&lt;/span&gt;&lt;span class="s2"&gt;bin\llamafile\llamafile.exe"&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"HOST=0.0.0.0"&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"PORT=8080"&lt;/span&gt;

&lt;span class="c"&gt;rem Optional shared password for the API/UI. Leave empty for no auth.&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"APIKEY=your-secret-key-here"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;exist&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;%BIN%&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="kd"&gt;llamafile&lt;/span&gt;&lt;span class="err"&gt;.exe&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kd"&gt;found&lt;/span&gt; &lt;span class="nb"&gt;at&lt;/span&gt;: &lt;span class="nv"&gt;%BIN%&lt;/span&gt;
    &lt;span class="nb"&gt;pause&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="na"&gt;/b &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;exist&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="o"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="kd"&gt;Model&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kd"&gt;found&lt;/span&gt; &lt;span class="nb"&gt;at&lt;/span&gt;: &lt;span class="nv"&gt;%MODEL%&lt;/span&gt;
    &lt;span class="nb"&gt;pause&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt; &lt;span class="na"&gt;/b &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;rem Detect this machine's LAN IP so other devices on the network can connect.&lt;/span&gt;
&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"LANIP="&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="na"&gt;/f &lt;/span&gt;&lt;span class="s2"&gt;"delims="&lt;/span&gt; &lt;span class="vm"&gt;%%a&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'powershell -NoProfile -Command "(Get-NetIPConfiguration &lt;/span&gt;&lt;span class="se"&gt;^|&lt;/span&gt;&lt;span class="s1"&gt; Where-Object {$_.IPv4DefaultGateway -ne $null -and $_.NetAdapter.Status -eq '&lt;/span&gt;&lt;span class="kd"&gt;Up&lt;/span&gt;&lt;span class="s1"&gt;'} &lt;/span&gt;&lt;span class="se"&gt;^|&lt;/span&gt;&lt;span class="s1"&gt; Select-Object -First 1 -ExpandProperty IPv4Address).IPAddress" 2&lt;/span&gt;&lt;span class="se"&gt;^&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;nul'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"LANIP=&lt;/span&gt;&lt;span class="vm"&gt;%%a&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;defined&lt;/span&gt; &lt;span class="kd"&gt;LANIP&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"LANIP=&amp;lt;this-laptop-ip&amp;gt;"&lt;/span&gt;

&lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"AUTH="&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;defined&lt;/span&gt; &lt;span class="kd"&gt;APIKEY&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt; &lt;span class="s2"&gt;"AUTH=--api-key &lt;/span&gt;&lt;span class="nv"&gt;%APIKEY%&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;==========================================================&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;  &lt;span class="kd"&gt;Portable&lt;/span&gt; &lt;span class="kd"&gt;AI&lt;/span&gt; &lt;span class="kd"&gt;server&lt;/span&gt; &lt;span class="kd"&gt;starting&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;==========================================================&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;   &lt;span class="kd"&gt;On&lt;/span&gt; &lt;span class="kd"&gt;this&lt;/span&gt; &lt;span class="kd"&gt;laptop&lt;/span&gt; : &lt;span class="kd"&gt;http&lt;/span&gt;://127.0.0.1:&lt;span class="nv"&gt;%PORT%&lt;/span&gt;/
&lt;span class="nb"&gt;echo&lt;/span&gt;   &lt;span class="kd"&gt;Other&lt;/span&gt; &lt;span class="kd"&gt;devices&lt;/span&gt;  : &lt;span class="kd"&gt;http&lt;/span&gt;://&lt;span class="nv"&gt;%LANIP%&lt;/span&gt;:&lt;span class="nv"&gt;%PORT%&lt;/span&gt;/
&lt;span class="nb"&gt;echo&lt;/span&gt;.
&lt;span class="nb"&gt;echo&lt;/span&gt;  &lt;span class="kd"&gt;Press&lt;/span&gt; &lt;span class="kd"&gt;Ctrl&lt;/span&gt;&lt;span class="na"&gt;+C &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt; &lt;span class="kd"&gt;stop&lt;/span&gt; &lt;span class="kd"&gt;the&lt;/span&gt; &lt;span class="kd"&gt;server&lt;/span&gt;.
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;==========================================================&lt;/span&gt;

&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;%BIN%&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="na"&gt;-m &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="na"&gt;--host &lt;/span&gt;&lt;span class="nv"&gt;%HOST%&lt;/span&gt; &lt;span class="na"&gt;--port &lt;/span&gt;&lt;span class="nv"&gt;%PORT%&lt;/span&gt; &lt;span class="nv"&gt;%AUTH%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few implementation notes worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;%~dp0&lt;/code&gt; resolves to the script's own directory&lt;/strong&gt; — this is the trick that makes the whole thing drive-letter-agnostic. It works whether Windows mounts your pendrive as &lt;code&gt;E:\&lt;/code&gt; or &lt;code&gt;G:\&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HOST=0.0.0.0&lt;/code&gt;&lt;/strong&gt; binds the server to all network interfaces, not just localhost — this is what lets other devices on the same wifi reach it. If you only want it accessible from the host laptop itself, set this to &lt;code&gt;127.0.0.1&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PowerShell one-liner&lt;/strong&gt; pulls the first "up" network adapter with a default gateway, a reliable way to grab the actual LAN-facing IP rather than a VPN or virtual adapter address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;APIKEY&lt;/code&gt;&lt;/strong&gt; gates access with a shared secret. Leave it blank for personal single-device use; set it if you're exposing the server to a shared network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Run It
&lt;/h2&gt;

&lt;p&gt;Double-click &lt;code&gt;run-portable-ai.bat&lt;/code&gt;. On first launch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Windows Firewall will prompt to allow &lt;code&gt;llamafile.exe&lt;/code&gt; — accept for &lt;strong&gt;Private networks&lt;/strong&gt; if you want other devices to reach it.&lt;/li&gt;
&lt;li&gt;The model loads into memory (a few seconds to a minute, depending on size and disk speed).&lt;/li&gt;
&lt;li&gt;Once ready, open &lt;code&gt;http://127.0.0.1:8080/&lt;/code&gt; in a browser — you'll see a built-in chat UI, ready to use, fully offline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To use it from your phone or another laptop on the same wifi, browse to the LAN address the script printed (e.g., &lt;code&gt;http://192.168.0.101:8080/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbzbyqoazx6vokwwvph9h.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%2Fbzbyqoazx6vokwwvph9h.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 (Optional): A Nicer Front-End
&lt;/h2&gt;

&lt;p&gt;llamafile's built-in UI is functional but basic. For a more polished chat experience, point a local UI like &lt;strong&gt;llama-ui&lt;/strong&gt; at the same endpoint — it talks to llamafile's OpenAI-compatible API (&lt;code&gt;/v1/chat/completions&lt;/code&gt;) and adds conversation history, model switching, and a settings panel for your API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Notes Before You Share This With Anyone
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Binding to &lt;code&gt;0.0.0.0&lt;/code&gt; with no API key means anyone on the same network can use your AI&lt;/strong&gt; — fine at home, not fine on public/office wifi. Set &lt;code&gt;APIKEY&lt;/code&gt; outside a fully trusted network.&lt;/li&gt;
&lt;li&gt;This setup is for &lt;strong&gt;personal, local, authorized use&lt;/strong&gt;. Don't port-forward it to the public internet without authentication and a reverse proxy — an open llamafile endpoint is an open compute resource for anyone who finds it.&lt;/li&gt;
&lt;li&gt;Quantized models can still produce inaccurate output. Treat it like any other LLM: verify anything factual before relying on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What This Actually Demonstrates
&lt;/h2&gt;

&lt;p&gt;This isn't just a neat trick — it's a working example of the shift happening in AI right now: inference moving from centralized cloud APIs to the edge. Quantization made 8B-parameter models small enough to run on a laptop CPU; single-binary engines like llamafile made deployment trivial enough that "portable AI on a USB stick" is now a weekend project instead of a research paper.&lt;/p&gt;

&lt;p&gt;For anyone thinking about data privacy, offline-capable tooling, or just wanting to understand LLM infrastructure hands-on instead of through an API wrapper — this is one of the most direct ways to get there.&lt;/p&gt;




&lt;p&gt;Have questions about adapting this for macOS/Linux, running a larger model, or securing it for multi-user access? Drop a comment below — happy to dig in.&lt;br&gt;
Sent&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Best Feature I Shipped Was Removing Friction: Rebuilding the UX of a Browser Video Editor</title>
      <dc:creator>MartinDelophy</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:30:27 +0000</pubDate>
      <link>https://dev.to/martindelophy/the-best-feature-i-shipped-was-removing-friction-rebuilding-the-ux-of-a-browser-video-editor-33k</link>
      <guid>https://dev.to/martindelophy/the-best-feature-i-shipped-was-removing-friction-rebuilding-the-ux-of-a-browser-video-editor-33k</guid>
      <description>&lt;p&gt;When I first shared &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;Timeline Studio&lt;/a&gt;, it had around 80 GitHub stars.&lt;/p&gt;

&lt;p&gt;The technical premise was already there: a local-first video editor that runs in the browser, with a multi-track timeline, voiceovers, captions, effects, and offline export.&lt;/p&gt;

&lt;p&gt;The user experience was not.&lt;/p&gt;

&lt;p&gt;Some early feedback was blunt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Interesting project, but the interface feels clunky.&lt;/p&gt;

&lt;p&gt;There are a lot of features, but some interactions feel unintuitive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That feedback was fair. I had spent too much time proving that browser video editing was possible and not enough time asking whether ordinary editing actions behaved the way people expected.&lt;/p&gt;

&lt;p&gt;The project has since passed 500 GitHub stars. The number is nice, but the more meaningful milestone was going back and fixing the small frustrations that made the entire editor feel harder than it needed to be.&lt;/p&gt;

&lt;p&gt;This is a post about those fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A feature can work and still be wrong
&lt;/h2&gt;

&lt;p&gt;The early version had split, zoom, scrolling, track locking, audio separation, mobile controls, and export settings.&lt;/p&gt;

&lt;p&gt;On paper, those features existed.&lt;/p&gt;

&lt;p&gt;In practice, they often exposed the implementation instead of matching the user's mental model.&lt;/p&gt;

&lt;p&gt;For example, the split action originally required an explicitly selected clip. A user could place the playhead in the middle of the main video, click the scissors button, and still be told to select a clip first.&lt;/p&gt;

&lt;p&gt;The code was consistent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selectedClip -&amp;gt; split(selectedClip, playheadTime)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interaction was not.&lt;/p&gt;

&lt;p&gt;The user was looking at the main visual under the playhead. They had already communicated enough intent.&lt;/p&gt;

&lt;p&gt;The current rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if a clip is explicitly selected:
    split that clip
else:
    find the main visual under the playhead
    select and split it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit selections are still respected, but the common action now has a useful default.&lt;/p&gt;

&lt;p&gt;This change added no new capability. It simply removed a question the editor should never have asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock the meaning of a continuous gesture
&lt;/h2&gt;

&lt;p&gt;Timeline editors usually need several wheel behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vertical track scrolling&lt;/li&gt;
&lt;li&gt;horizontal timeline panning&lt;/li&gt;
&lt;li&gt;timeline zoom&lt;/li&gt;
&lt;li&gt;Shift-modified panning&lt;/li&gt;
&lt;li&gt;Ctrl/Cmd wheel zoom&lt;/li&gt;
&lt;li&gt;trackpad pinch zoom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My initial implementation kept resolving the wheel behavior from whatever happened to be under the pointer.&lt;/p&gt;

&lt;p&gt;That sounds reasonable until the timeline moves.&lt;/p&gt;

&lt;p&gt;Imagine starting a vertical scroll over empty timeline space. As the tracks move, a clip slides under the stationary pointer. If the editor resolves the target again, the gesture can suddenly change from scrolling tracks to zooming the timeline.&lt;/p&gt;

&lt;p&gt;From the user's perspective, the editor has changed its mind halfway through an action.&lt;/p&gt;

&lt;p&gt;The fix was to treat a continuous wheel sequence as one gesture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gestureTarget = hitTest(pointerPositionAtStart)
gestureMode = resolveMode(gestureTarget, modifierKeys)

while the gesture continues:
    keep gestureMode stable

after a short pause:
    allow the next gesture to resolve again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting behavior is predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start over empty timeline space to scroll track lanes&lt;/li&gt;
&lt;li&gt;start over a clip to zoom the timeline&lt;/li&gt;
&lt;li&gt;use Shift for horizontal panning&lt;/li&gt;
&lt;li&gt;use Ctrl/Cmd or pinch for explicit zoom&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is not the exact mapping. It is that the mapping does not mutate while the user is still performing the same physical gesture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Users manipulate clips, not source asset IDs
&lt;/h2&gt;

&lt;p&gt;A split video often produces several timeline clips that reference the same source file.&lt;/p&gt;

&lt;p&gt;Internally, that might look 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;sourceAssetId: video-001

clip A -&amp;gt; source range 0s–5s
clip B -&amp;gt; source range 5s–12s
clip C -&amp;gt; source range 12s–20s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An early version of "Separate audio" relied too heavily on the shared source asset ID. That made whole-asset matching convenient, but it violated what the user had actually asked for.&lt;/p&gt;

&lt;p&gt;When the command comes from clip B's context menu, the expected result is the audio for 5s–12s—not new mappings for every sibling clip backed by &lt;code&gt;video-001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The current behavior operates on the exact timeline segment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserve its source trim&lt;/li&gt;
&lt;li&gt;preserve its playback speed&lt;/li&gt;
&lt;li&gt;place the extracted audio on an audio lane&lt;/li&gt;
&lt;li&gt;mute the corresponding picture-in-picture clip to prevent doubled sound&lt;/li&gt;
&lt;li&gt;select and reveal the resulting audio&lt;/li&gt;
&lt;li&gt;leave untouched sibling clips alone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whole-asset matching still makes sense when an unsplit source asset is intentionally dragged onto a source-audio track. It does not make sense for a clip-scoped menu action.&lt;/p&gt;

&lt;p&gt;The lesson was simple: internal identity is not the same thing as interaction scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic layout should not rearrange confirmed work
&lt;/h2&gt;

&lt;p&gt;Audio lane placement caused a similar problem.&lt;/p&gt;

&lt;p&gt;When a new voice clip overlapped existing audio, an automatic placement algorithm could move an existing clip to another lane. The layout became technically valid, but the user's previously arranged work changed without permission.&lt;/p&gt;

&lt;p&gt;Now existing clips stay where they are.&lt;/p&gt;

&lt;p&gt;New voice or audio clips find another available lane, or a new ordinary audio lane is materialized when necessary. AI music always routes to the dedicated music track. Split audio can still be moved vertically between normal audio lanes, but adding something new never displaces something already confirmed.&lt;/p&gt;

&lt;p&gt;Automation should solve the new placement problem, not reopen old decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the primary visual in view
&lt;/h2&gt;

&lt;p&gt;Once a project contains overlays, captions, source audio, voiceovers, and music, vertical scrolling becomes unavoidable.&lt;/p&gt;

&lt;p&gt;Letting every track scroll together meant the main visual could disappear while the user worked on lower lanes. That removed the most important reference point in the composition.&lt;/p&gt;

&lt;p&gt;The main visual track is now pinned directly below the ruler. Overlay and lower tracks scroll underneath it.&lt;/p&gt;

&lt;p&gt;The persistent scrollbar chrome is hidden, but both horizontal and vertical scrolling remain functional. The pinned track's lower edge also acts as the upper boundary for vertical drag auto-scroll, so cross-lane moves still behave correctly.&lt;/p&gt;

&lt;p&gt;This required more than &lt;code&gt;position: sticky&lt;/code&gt;. Pointer hit testing, drag targets, auto-scroll boundaries, and main-to-overlay or overlay-to-main moves all had to use the same geometry.&lt;/p&gt;

&lt;p&gt;The visible result is much less dramatic: the main picture simply stops getting lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile is not desktop with smaller buttons
&lt;/h2&gt;

&lt;p&gt;The first mobile layout was essentially the desktop editor compressed into a narrow viewport.&lt;/p&gt;

&lt;p&gt;It technically contained the same features. It was also exhausting to use.&lt;/p&gt;

&lt;p&gt;The redesigned mobile workspace follows a different hierarchy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preview first&lt;/li&gt;
&lt;li&gt;timeline immediately below it&lt;/li&gt;
&lt;li&gt;a centered playhead with the timeline moving around it&lt;/li&gt;
&lt;li&gt;clip-specific actions after selection&lt;/li&gt;
&lt;li&gt;properties inside a focused bottom drawer&lt;/li&gt;
&lt;li&gt;touch-sized controls&lt;/li&gt;
&lt;li&gt;no desktop keyboard-shortcut guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Desktop and mobile still edit the same project model. They do not need to expose that model through the same interface.&lt;/p&gt;

&lt;p&gt;Responsive design is not preserving every panel at every width. It is preserving the user's task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small state mismatches create large trust problems
&lt;/h2&gt;

&lt;p&gt;Several fixes were almost embarrassingly small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clicking the Export button a second time now closes the export popover.&lt;/li&gt;
&lt;li&gt;Icon-only timeline controls have localized tooltips on hover and keyboard focus.&lt;/li&gt;
&lt;li&gt;Locked tracks retain a subdued treatment, but their clips no longer gain a dashed border that looks like selection.&lt;/li&gt;
&lt;li&gt;"Analyze person" and "Analyze object" use the primary action style whenever they are actionable; the neutral style is reserved for "Cancel analysis."&lt;/li&gt;
&lt;li&gt;Generated music and converted voices go to the asset library instead of silently modifying the timeline.&lt;/li&gt;
&lt;li&gt;Repeated voice generations append after the current voiceover end instead of stacking at 0 seconds or an unchanged playhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these belongs in an impressive model benchmark.&lt;/p&gt;

&lt;p&gt;Together, they answer a more important question: can the user predict what will happen after the next click?&lt;/p&gt;

&lt;h2&gt;
  
  
  The product changed when the question changed
&lt;/h2&gt;

&lt;p&gt;At the beginning, I kept asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this run in a browser?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Can WebCodecs export the composition? Can WebGPU run the model? Can the timeline support another track type? Can an agent modify the project?&lt;/p&gt;

&lt;p&gt;Those were useful questions, but they produced a technology demonstration.&lt;/p&gt;

&lt;p&gt;The editor started becoming a product when the question changed to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does the user reasonably expect to happen here?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question led to fewer interruptions, stable gestures, clip-scoped actions, conservative automation, clearer state, and a mobile interface built around touch rather than CSS breakpoints.&lt;/p&gt;

&lt;p&gt;Timeline Studio is still evolving. It combines a real multi-track editor with local-first browser AI, portable &lt;code&gt;.timeline&lt;/code&gt; projects, and deterministic offline export. There are still plenty of rough edges to find.&lt;/p&gt;

&lt;p&gt;But if you tried an early version and found the UI clunky, I would genuinely like to know whether the same problems still stand—and what remains confusing today.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/MartinDelophy/ai-video-editor" rel="noopener noreferrer"&gt;https://github.com/MartinDelophy/ai-video-editor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live editor:&lt;/strong&gt; &lt;a href="https://video-editor.ai-creator.top/" rel="noopener noreferrer"&gt;https://video-editor.ai-creator.top/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful feedback is not "add another AI model."&lt;/p&gt;

&lt;p&gt;It is: "I expected this action to do X, but it did Y."&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I made 50 free dark themes for Claude Code. Here is what I learned about semantic terminal colors.</title>
      <dc:creator>Gabriel García Alonso</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:19:35 +0000</pubDate>
      <link>https://dev.to/gabriel_garcaalonso_7d0/i-made-50-free-dark-themes-for-claude-code-here-is-what-i-learned-about-semantic-terminal-colors-1179</link>
      <guid>https://dev.to/gabriel_garcaalonso_7d0/i-made-50-free-dark-themes-for-claude-code-here-is-what-i-learned-about-semantic-terminal-colors-1179</guid>
      <description>&lt;p&gt;I made a set of 50 free dark themes for Claude Code. The source, install instructions and all 50 theme files are here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/notgabriels-sys/claude-code-50-dark-themes" rel="noopener noreferrer"&gt;https://github.com/notgabriels-sys/claude-code-50-dark-themes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famd5uzohnztjrglp9sdj.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%2Famd5uzohnztjrglp9sdj.png" alt="50 Dark Themes for Claude Code project preview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This started as a small personal project. I wanted Claude Code to feel more comfortable during long sessions, but I did not want themes that only looked good as color swatches and then made real terminal states harder to read.&lt;/p&gt;

&lt;p&gt;After making the set, the useful lesson was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dark themes are not just background colors.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The main design choice: do not repaint every semantic color
&lt;/h2&gt;

&lt;p&gt;A lot of dark palettes look fine as static color cards, but developer tools need meaning. A warning, an error, a success state, a diff addition and a diff deletion should not collapse into the same decorative accent.&lt;/p&gt;

&lt;p&gt;The important implementation detail in this project is that every theme inherits Claude Code's base dark preset and overrides only a controlled set of interface roles. The &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt; and diff colors are intentionally left to the base preset, so they keep their semantic green, red and yellow meanings.&lt;/p&gt;

&lt;p&gt;Here is an abbreviated version of &lt;code&gt;amber-room.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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Amber Room"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"base"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"overrides"&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;"claude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#E0A83C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#EDEBE8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inactive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#AFA79D"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subtle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#342F27"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"promptBorder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#E0A83C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userMessageBackground"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#2C2821"&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;That limited override surface is deliberate. It lets the visual identity change without turning status colors into decoration.&lt;/p&gt;

&lt;p&gt;The roles I kept checking were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;errors should still feel like errors&lt;/li&gt;
&lt;li&gt;warnings should remain visually distinct from errors&lt;/li&gt;
&lt;li&gt;success states should not look like generic decoration&lt;/li&gt;
&lt;li&gt;diff additions and deletions need enough contrast from each other&lt;/li&gt;
&lt;li&gt;muted text should be readable but not compete with primary output&lt;/li&gt;
&lt;li&gt;the background should support long sessions, not dominate them&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Contrast hierarchy matters more than vibe
&lt;/h2&gt;

&lt;p&gt;The difficult part was usually not choosing a hue. It was keeping the hierarchy readable across primary text, inactive text, subtle surfaces, prompt borders and raised user-message backgrounds.&lt;/p&gt;

&lt;p&gt;The safer combinations had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a calm dark surface&lt;/li&gt;
&lt;li&gt;foreground text that stayed readable&lt;/li&gt;
&lt;li&gt;muted text that did not disappear&lt;/li&gt;
&lt;li&gt;accents that worked in small UI labels, not only in big preview blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The funny part is that the “loudest” palettes often became less useful than the restrained ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminal background matters
&lt;/h2&gt;

&lt;p&gt;Claude Code cannot set the terminal application's background color, so that remains a separate terminal setting. For the full effect, I set the terminal background to each theme's surface hex.&lt;/p&gt;

&lt;p&gt;Without that, the palette can still work, but the theme does not feel fully integrated. The background is not just decoration; it is part of the reading environment.&lt;/p&gt;

&lt;p&gt;That is also why the repository documents the matching surface value. A theme is not only a few accent swatches; it is a working space.&lt;/p&gt;

&lt;h2&gt;
  
  
  The install path had to stay boring
&lt;/h2&gt;

&lt;p&gt;For a visual project, the temptation is to spend all the effort on the gallery. But the install flow matters more.&lt;/p&gt;

&lt;p&gt;The project supports the native Claude Code plugin marketplace flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add notgabriels-sys/claude-code-50-dark-themes
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;50-dark-themes@notgabriels-themes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The README also keeps a manual copy-to-&lt;code&gt;~/.claude/themes/&lt;/code&gt; path visible. That matters because a theme pack is only useful if someone can install it without guessing.&lt;/p&gt;

&lt;p&gt;The basic lesson: if the thing is free and practical, the path to use it should be practical too.&lt;/p&gt;

&lt;h2&gt;
  
  
  I kept the themes free
&lt;/h2&gt;

&lt;p&gt;The 50 Claude Code themes are free and MIT licensed.&lt;/p&gt;

&lt;p&gt;If you use Claude Code and care about readable dark UI, I would genuinely like feedback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which themes feel comfortable for long sessions?&lt;/li&gt;
&lt;li&gt;Which ones look good but fail in real use?&lt;/li&gt;
&lt;li&gt;What semantic color roles should be improved?&lt;/li&gt;
&lt;li&gt;Are there terminal/editor combinations where the contrast breaks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are the complete theme list, source files and install instructions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/notgabriels-sys/claude-code-50-dark-themes" rel="noopener noreferrer"&gt;https://github.com/notgabriels-sys/claude-code-50-dark-themes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I used AI assistance to structure and edit this article. I reviewed the technical claims, example configuration and links against the public repository before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>terminal</category>
      <category>tooling</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to catch the numbers your provider's model makes up, with a second model in your cluster</title>
      <dc:creator>Antonio Berben</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:08:42 +0000</pubDate>
      <link>https://dev.to/antonioberben/how-to-catch-the-numbers-your-providers-model-makes-up-with-a-second-model-in-your-cluster-2ii5</link>
      <guid>https://dev.to/antonioberben/how-to-catch-the-numbers-your-providers-model-makes-up-with-a-second-model-in-your-cluster-2ii5</guid>
      <description>&lt;p&gt;&lt;strong&gt;A hands-on lab. Hallucinated figures are the checkable kind: precision without a source. Your provider keeps generating; the catching stays in your cluster.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobk4d80hqlvdyux908pb.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%2Fobk4d80hqlvdyux908pb.png" alt="Your provider invents numbers. Catch them at home: a second model, running in your own cluster, flags the figures nobody can back up" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask a hosted model for an analyst briefing on bank fraud for next year, and tell it you want concrete figures. It will write you this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...it is estimated that banks will reduce fraud-related losses by 30%, translating to savings of about €1.2 billion across the sector...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody has next year's numbers. There is no source, no hedge, and it reads like something you could paste into a deck on Monday.&lt;/p&gt;

&lt;p&gt;What catches that here is not a fact checker. It is a second model, small enough to run on a CPU in your own cluster, asked one question about the answer: does it state specifics it cannot possibly support? It replies with a number and one line:&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="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Several unsourced specifics, stated as fact"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole idea, and it is worth being precise about what it is not. The second model has no way of knowing whether 30% is true; it never sees the question, and it has no sources. What it judges is whether the answer had any business stating that figure at all. Vague is fine. &lt;strong&gt;Unsourced precision is not.&lt;/strong&gt; That is the narrow, checkable slice of what everyone calls hallucination, and it happens to be the slice that reaches your users dressed as a deliverable.&lt;/p&gt;

&lt;p&gt;The interesting decision is not whether to do this. It is where that second model runs. Some version of this conversation has happened to me more than once. Compressed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform team:&lt;/strong&gt; we added a judge at the gateway. Every answer the agent produces gets scored by a second model, and the scores land in a dashboard. We did not touch the agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me:&lt;/strong&gt; nice. Which model does the scoring?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform team:&lt;/strong&gt; a hosted one. It was the quickest to wire up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; and where does the answer travel to, in order to be scored?&lt;/p&gt;

&lt;p&gt;That last one rarely has a good answer. Not because anyone was careless: the pattern is right, and the design review passes. What bites you is one field in a webhook config, and it never shows up on the diagram.&lt;/p&gt;

&lt;p&gt;Let's build it with that second model inside your own cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern has a name, and a catch
&lt;/h2&gt;

&lt;p&gt;What I just described has a name, LLM-as-a-judge, and it is worth knowing where it came from, because the origin explains a constraint people keep tripping over. The term arrives with &lt;a href="https://arxiv.org/abs/2306.05685" rel="noopener noreferrer"&gt;Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena&lt;/a&gt; (Zheng et al., 2023), and MT-Bench and Chatbot Arena are &lt;strong&gt;benchmarks&lt;/strong&gt;. This was built to rank chat assistants against human preference, offline, in batch, with nobody waiting.&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%2F93k6v24ncs7dj8pzt8zb.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%2F93k6v24ncs7dj8pzt8zb.png" alt="LLM-as-a-judge started as an offline benchmark: the answer goes to the user and a copy is graded on the side" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The judge started life off the critical path: the answer goes to the user, and a copy gets graded on the side.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Notice where the judge sits in that picture. Not between the model and the user. Off to one side, reading a copy, writing to a scoreboard the user never sees. That is the shape the pattern was designed in, and it is why nobody in 2023 worried about what a judge does to your p99.&lt;/p&gt;

&lt;p&gt;The same paper is also useful for the part people skip. It measures the biases: position, verbosity, and &lt;strong&gt;self-enhancement&lt;/strong&gt;, the tendency of a model to prefer its own output. Which is the empirical reason the judge has to be a &lt;em&gt;different&lt;/em&gt; model, rather than just a good instinct. Ask a model whether its own answer was any good and it will tell you yes, warmly and at length.&lt;/p&gt;

&lt;p&gt;So the pattern is sound and boring. What it does not tell you is anything about the deployment it leaves open, and that is the whole of the rest of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the gateway, and not inside the app
&lt;/h2&gt;

&lt;p&gt;The obvious place is the application: you already have the answer in a variable. That holds for one app. By the third, the rubric, the threshold and the judge's credentials live in three codebases drifting at three speeds, and nobody can tell you which rubric was applied to the answer somebody is complaining about.&lt;/p&gt;

&lt;p&gt;Those three apps already send every model call through the same gateway, so put it there once. agentgateway, one of the open source projects under the Agentic AI Foundation, has the hook: &lt;code&gt;promptGuard&lt;/code&gt; inspects requests on the way in and responses on the way out, and hands them to a webhook you write.&lt;/p&gt;

&lt;p&gt;One thing changes, and it is fairer to say it now than let you find it in Step 6: the judge is no longer off to one side. It sits in the path, and your user waits for it. You are trading a benchmark for an enforcement point, and latency is on the price tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hole
&lt;/h2&gt;

&lt;p&gt;Here is the thing about a judge. You can rate-limit a generator. You can cache it, you can sample it, you can put it behind a paywall and most of your users will never hit it hard.&lt;/p&gt;

&lt;p&gt;A judge is different. A judge reads &lt;strong&gt;everything&lt;/strong&gt;, by construction. Every answer, every time, including the ones the user abandoned halfway, including the draft that got masked before delivery, including the one where the model helpfully restated the customer's account details back to them.&lt;/p&gt;

&lt;p&gt;Point that at a third-party API and you have not added a guardrail. You have added a second, quieter copy of your entire output stream, flowing somewhere you do not control.&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%2Fu7y6jg1cylkbg5ms78fo.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%2Fu7y6jg1cylkbg5ms78fo.png" alt="agentgateway inspects every response on the way out, so a judge wired to a third-party API takes every one of them with it" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;agentgateway inspects every response on the way out. Wire the judge to a third-party API and every one of those responses leaves with it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am based in Europe, so this lands on my desk with paperwork attached, and the paperwork moved this summer. Regulation (EU) 2026/1744, the digital omnibus, pushed the high-risk obligations for standalone Annex III systems from 2 August 2026 to &lt;strong&gt;2 December 2027&lt;/strong&gt;. What they ask for did not change, only when they start asking.&lt;/p&gt;

&lt;p&gt;Two of them matter here. Annex III 5(b) makes creditworthiness and credit scoring high-risk, with fraud detection explicitly carved out, so it is the lending side of the bank that qualifies and not the fraud model I keep using for demo prompts. Article 12 requires high-risk systems to allow "the automatic recording of events (logs) over the lifetime of the system", and Article 26(6) puts keeping those logs on the deployer, for at least six months.&lt;/p&gt;

&lt;p&gt;Which is convenient, actually: a judge that scores every answer and logs the verdict is close to the evidence the regulation wants, not overhead stacked on top of it. It only makes it dafter to build that trail by streaming customer answers through somebody else's inference endpoint, where GDPR will ask about processors long before the AI Act gets a turn.&lt;/p&gt;

&lt;p&gt;And the sixteen extra months are not a reason to close the tab. That is roughly one platform roadmap, which is exactly the difference between designing the evidence trail into the request path and bolting it on later while somebody from legal watches you do it.&lt;/p&gt;

&lt;p&gt;Not a lawyer, not legal advice. The architectural point: the component with the most complete view of your output is the one I am least willing to rent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same policy, judge moved in-house
&lt;/h2&gt;

&lt;p&gt;The fix is not a redesign, and it is not "stop using hosted models" either. Keep your provider exactly where it is. Move only the judge.&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%2Fmstnf0hrvr8l5scjnzao.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%2Fmstnf0hrvr8l5scjnzao.png" alt="The prompt still goes to your provider; only the grading happens in the cluster, on a model you serve yourself" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The prompt still goes to your provider. Only the grading happens in the cluster, on a model you serve yourself.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the generator stays remote: whatever frontier model your agent already uses, because that is the part where paying for quality makes sense. The judge comes home, served in the cluster by Ollama, because grading against a rubric is a far smaller job than writing the answer. I ran this with &lt;code&gt;qwen2.5:3b&lt;/code&gt;. I started smaller and had to go up, and Step 3 is where that story lives.&lt;/p&gt;

&lt;p&gt;That split is doing two things at once, and the second one is easy to miss. Every answer stops leaving your network twice: the provider sees the traffic it was always going to see, and the complete copy nobody accounted for never happens. And your provider bill does not double, which it would if the judge were hosted too. You add a second inference per answer, but that second one runs on hardware you already pay for.&lt;/p&gt;

&lt;p&gt;Ollama is the lab choice because it is two commands. For real traffic you want vLLM on a GPU node, which is the same picture with a different Deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everything below is a transcript, not a plan. I ran it on a kind cluster with &lt;code&gt;gpt-4o-mini&lt;/code&gt; generating and &lt;code&gt;qwen2.5:3b&lt;/code&gt; judging, and the outputs are copied from that run, including every place where my first attempt was wrong. One exception, so you are not surprised when you run it: after that run I renamed two strings the webhook prints, to stop calling the same check three different things. The scores, the reasons and everything else are exactly as they came out. Every version is pinned in the demo's &lt;code&gt;.env&lt;/code&gt;, which is the one place to look when you want to know exactly what I installed, and the one place to change when you want something newer. I am deliberately not repeating those numbers through the article: a version quoted in prose is a version that goes stale without anybody noticing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What you need, and how long this takes
&lt;/h2&gt;

&lt;p&gt;Seven steps, and they go: cluster and gateway, judge model, provider, webhook, policy, the three enforcement modes, then the bill. The guardrail is live at Step 4. Everything after that is choosing how hard it bites.&lt;/p&gt;

&lt;p&gt;You need &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;helm&lt;/code&gt;, &lt;code&gt;jq&lt;/code&gt;, a cluster (&lt;code&gt;kind&lt;/code&gt; is fine), and an API key for whichever provider you route to. Then the requirement that actually decides whether this works: &lt;strong&gt;a node with room for a 3b model resident&lt;/strong&gt;. The Ollama Deployment in the lab asks for 2 CPUs and 3 GB, with a 6 GB ceiling, plus a couple of gigabytes of disk for the weights. On Docker Desktop that is a slider you probably have to move before you start, and a pod sitting in &lt;code&gt;Pending&lt;/code&gt; at Step 1 is nearly always this and nothing more interesting.&lt;/p&gt;

&lt;p&gt;Half an hour, of which maybe five minutes is you typing. The model download is 1.9 GB. Once the model is loaded and stays loaded, grading an answer on CPU costs well under a second; the first call after it has been unloaded costs eleven, and Step 1 explains why that number matters more than the small one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: a cluster and a gateway
&lt;/h2&gt;

&lt;p&gt;The manifests, the webhook and the &lt;code&gt;.env&lt;/code&gt; are in &lt;a href="https://github.com/antonioberben/kagent-examples/tree/main/demos/0041-llm-as-a-judge-local" rel="noopener noreferrer"&gt;antonioberben/kagent-examples, demo 0041&lt;/a&gt;. Start there, because every command below reads its versions and names from that file rather than carrying them inline, which is the only reason this article is still runnable a year after I wrote it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/antonioberben/kagent-examples
&lt;span class="nb"&gt;cd &lt;/span&gt;kagent-examples/demos/0041-llm-as-a-judge-local

&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="nv"&gt;$EDITOR&lt;/span&gt; .env                     &lt;span class="c"&gt;# OPENAI_API_KEY is the only value you must fill in&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the cluster. And pin the context, once, so nothing below can wander into the wrong one. If you are the sort of person who has twenty kind clusters lying around, that second line is not optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
kubectl config use-context &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$KUBE_CONTEXT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;agentgateway builds on the Gateway API, so those CRDs go in first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;--server-side&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/kubernetes-sigs/gateway-api/releases/download/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_API_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/standard-install.yaml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the control plane. Quickstarts for fast-moving projects tend to hand you a rolling main-line tag, and this is where you decide not to take it. The stable docs pin a real release now; the main-line pages still say &lt;code&gt;0.0.0-latest-dev&lt;/code&gt;, and those are the ones you land on when you go looking for the newest thing. It is worth understanding what copying that costs you: two people following the same instructions a month apart install different software, and neither of them can tell you which. Pin a real release instead, in &lt;code&gt;.env&lt;/code&gt;, once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; agentgateway-crds oci://cr.agentgateway.dev/charts/agentgateway-crds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="nt"&gt;--namespace&lt;/span&gt; agentgateway-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENTGATEWAY_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; agentgateway oci://cr.agentgateway.dev/charts/agentgateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; agentgateway-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENTGATEWAY_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the GatewayClass registered itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get gatewayclass agentgateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a proxy. This is a plain Gateway API resource pointing at the &lt;code&gt;agentgateway&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: agentgateway-proxy
  namespace: agentgateway-system
spec:
  gatewayClassName: agentgateway
  listeners:
  - protocol: HTTP
    port: 80
    name: http
    allowedRoutes:
      namespaces:
        from: All
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system rollout status deploy/agentgateway-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;kind does not do &lt;code&gt;LoadBalancer&lt;/code&gt; services, so forward the port and leave it running in another terminal. You will open three terminals before this is over, and each one needs the environment loaded again, so that first line is not decoration:&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="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .env &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system port-forward svc/agentgateway-proxy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to know now rather than at Step 5: a &lt;code&gt;port-forward&lt;/code&gt; dies with the pod behind it, and this lab restarts the webhook three times on purpose. When a &lt;code&gt;curl&lt;/code&gt; suddenly returns nothing at all, the tunnel is what broke, not the guardrail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: serve the judge model in the cluster
&lt;/h2&gt;

&lt;p&gt;This is the part every "put a judge on it" article skips, and it is the part that decides whether the rest of this is an architecture or a slide.&lt;/p&gt;

&lt;p&gt;Ollama goes in as an ordinary Deployment plus a ClusterIP Service. Nothing in agentgateway needs to know it exists: only the webhook will talk to it, over plain HTTP, on an in-cluster DNS name.&lt;/p&gt;

&lt;p&gt;One environment variable in there matters more than it looks, and I had it wrong for a while:&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;env&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;OLLAMA_KEEP_ALIVE&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ollama unloads an idle model after five minutes. I originally set this to &lt;code&gt;30m&lt;/code&gt;, thinking of it as a latency optimisation: a cold load in front of a user who is already waiting is the second you did not budget for. Then I left the lab alone overnight and the guardrail stopped working, which is how I found out it is not a latency optimisation at all.&lt;/p&gt;

&lt;p&gt;Here is the same request three times in a row, from inside the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;call 1   11.6s     model cold, loaded from disk
call 2    0.6s
call 3    0.6s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eleven seconds is over agentgateway's limit. It caps the guardrail webhook call at ten, so a cold model does not make your user wait: it makes the guardrail time out, fail open, and let the answer through ungraded. Your first request after an idle period is the one that skips the check, and nothing in the response says so.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;-1&lt;/code&gt;, which keeps it loaded for good. On a shared node you may not want that, and then the honest alternative is a warm-up call on a timer, not a bigger &lt;code&gt;JUDGE_TIMEOUT&lt;/code&gt;: your webhook's timeout cannot buy you time the gateway is not willing to wait.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/01-ollama.yaml
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system rollout status deploy/ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now pull the judge. A real download, 1.9 GB, a couple of minutes on a normal connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/ollama &lt;span class="nt"&gt;--&lt;/span&gt; ollama pull &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JUDGE_MODEL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nb"&gt;exec &lt;/span&gt;deploy/ollama &lt;span class="nt"&gt;--&lt;/span&gt; ollama list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME          ID              SIZE      MODIFIED
qwen2.5:3b    357c53fb659c    1.9 GB    About a minute ago
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you write a line of webhook code, talk to the judge the way the webhook will. Ollama serves an OpenAI-compatible endpoint, and that is the entire interface between your code and the model, which is also why swapping in vLLM later leaves every other component in this lab untouched. Forward it in a spare terminal and ask it something trivial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system port-forward svc/ollama 11434:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:11434/v1/chat/completions &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{
  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$JUDGE_MODEL&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,
  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;messages&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: [{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;content&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Reply with the single word OK&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}]
}"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One word, and it took a while: this is the first call after the pull, so you are paying the cold load from a few paragraphs ago before you get your &lt;code&gt;OK&lt;/code&gt;. Ask again and it comes back instantly. If that came back empty, stop here: everything after this point assumes the judge answers, and debugging a guardrail is much harder than debugging a &lt;code&gt;curl&lt;/code&gt;. Close that forward once it works, you will not need it again.&lt;/p&gt;

&lt;p&gt;The manifest uses an &lt;code&gt;emptyDir&lt;/code&gt;, so deleting the pod means pulling again. Fine for a first pass, swap in a PVC before anyone else depends on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: point agentgateway at your provider
&lt;/h2&gt;

&lt;p&gt;The generator is whatever you already use. The key goes in a Secret, and agentgateway reads it from there rather than from your agent's environment, which is half the reason to put a gateway in front of models at all:&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="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-rsp&lt;/span&gt; &lt;span class="s1"&gt;'provider API key: '&lt;/span&gt; PROVIDER_KEY &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo

&lt;/span&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system create secret generic openai-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;Authorization&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROVIDER_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;unset &lt;/span&gt;PROVIDER_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what those three lines buy you: the key never touches a manifest, and &lt;code&gt;read -rs&lt;/code&gt; keeps it out of your shell history. It costs nothing and it is how these labs stop ending up on GitHub with a live key in them. If you would rather keep it in &lt;code&gt;.env&lt;/code&gt;, that works too and &lt;code&gt;test.sh&lt;/code&gt; will read it from there. One prompt fewer, one more file on your laptop holding a live key. Pick whichever of those two you dislike less.&lt;/p&gt;

&lt;p&gt;Then the backend, which is about as short as this API gets:&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;agentgateway.dev/v1alpha1&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;AgentgatewayBackend&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="s"&gt;openai&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&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;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;
  &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;secretRef&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;openai-secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;policies&lt;/code&gt; is a sibling of &lt;code&gt;ai&lt;/code&gt;, not a child of it. Swapping providers later is this block and nothing else: the agent, the webhook and the policy all stay untouched.&lt;/p&gt;

&lt;p&gt;The route is unremarkable except for two details, and both of them bite later. Its &lt;strong&gt;name matters&lt;/strong&gt;, because the guardrail policy targets it by name. And it carries an explicit &lt;code&gt;matches&lt;/code&gt; on the &lt;code&gt;/v1&lt;/code&gt; prefix rather than accepting everything, which looks like tidiness and is not: I will show you in Step 6 what a catch-all in front of a metered provider actually costs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/02-backend-route.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it something:&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "model": "gpt-4o-mini",
  "messages": [
    {"role": "user", "content": "In two sentences, how is AI changing fraud detection in retail banking?"}
  ]
}'&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;model&lt;/code&gt; field is there because the OpenAI schema wants one, and for no other reason. Put &lt;code&gt;does-not-exist-9000&lt;/code&gt; in it and you still get a perfectly good answer from &lt;code&gt;gpt-4o-mini&lt;/code&gt;: the backend decides which model runs, not the caller. Worth knowing before you spend an afternoon wondering why the field you changed had no effect, and worth knowing for the better reason too, which is that your agents cannot quietly route themselves to a model you did not approve.&lt;/p&gt;

&lt;p&gt;You get a sensible-looking paragraph, produced remotely, and you have no way of knowing whether any of it is true. That is the gap, and closing it is the only thing left that has to happen locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the judge webhook, and the contract nobody documents well
&lt;/h2&gt;

&lt;p&gt;Before writing any code, look at what agentgateway is actually going to send you. Two paths, one JSON envelope, one action back.&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%2Fq05zjuoqn7cudo3e9bo2.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%2Fq05zjuoqn7cudo3e9bo2.png" alt="The guardrail webhook contract: two endpoints, one JSON envelope, and one action whose shape decides pass, mask or reject" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two endpoints, one JSON envelope, one action back. The shape of that action is what decides the user's fate.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The request phase posts to &lt;code&gt;/request&lt;/code&gt;, the response phase to &lt;code&gt;/response&lt;/code&gt;. Those are defaults rather than laws, in case you are wiring this into a guardrail service that already owns its URL layout: a CEL expression on the &lt;code&gt;:path&lt;/code&gt; pseudo-header moves them. For a webhook you are writing anyway, take the defaults. On the response phase you receive this:&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;"body"&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;"choices"&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;"message"&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;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&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="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;p&gt;Stare at that for a moment, because there is something missing and it changes your rubric.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The original question is not there.&lt;/strong&gt; The response envelope carries the choices and nothing else. So a response-phase judge cannot grade "did this answer the question", because it has never seen the question. Half the LLM-as-a-judge rubrics you find online are ungradeable in this position, and they will happily return a confident number anyway.&lt;/p&gt;

&lt;p&gt;That is not a defect, it is a scoping decision, and once you accept it the rubric gets sharper. Grade a property the answer carries on its own, and pick one you can check by reading: does it state specifics nobody could source? That is precisely what catches a model inventing quarterly revenue figures, and Step 3 is also where I show you the version of this sentence I got wrong first. If you need question-relative grading, register a &lt;code&gt;request&lt;/code&gt; guard too and correlate the two calls yourself.&lt;/p&gt;

&lt;p&gt;Now the reply. You answer &lt;code&gt;200&lt;/code&gt;, always, with one &lt;code&gt;action&lt;/code&gt; object, and here is the part worth tattooing somewhere: on agentgateway's side &lt;code&gt;action&lt;/code&gt; is an &lt;strong&gt;untagged union&lt;/strong&gt;. There is no &lt;code&gt;type&lt;/code&gt; field. The decision is inferred from the shape of what you send.&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="nl"&gt;"action"&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="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score 4/5"&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;Only &lt;code&gt;reason&lt;/code&gt; present, so pass. The answer goes out untouched.&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="nl"&gt;"action"&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="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="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"choices"&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="err"&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verdict appended"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;body&lt;/code&gt; is an object, so mask. Your rewritten choices replace the originals.&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="nl"&gt;"action"&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="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;"This answer was withheld: it states figures without a source."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;body&lt;/code&gt; is a string and there is a &lt;code&gt;status_code&lt;/code&gt;, so reject.&lt;/p&gt;

&lt;p&gt;Get a field name wrong and you do not get an error, you get a different decision. Which is the kind of API worth seeing drawn once before writing against it.&lt;/p&gt;

&lt;p&gt;The webhook itself is about 200 lines of Python standard library. No SDK, no gRPC, no pip install at pod startup. The interesting parts:&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;extract_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&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="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;choice&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the rubric. This is the part I got wrong twice, and it is worth your five minutes because the failure was silent both times.&lt;/p&gt;

&lt;p&gt;My first rubric asked for three criteria — accuracy, grounding, caution — with pleasant descriptions like "are specific figures plausibly knowable". Reasonable-looking prompt. Here is what &lt;code&gt;qwen2.5:1.5b&lt;/code&gt; did with it, on three answers I wrote to be easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FABRICATED (want low)   {"accuracy":4,"grounding":4,"caution":4} "figures are plausible knowable information"
HONEST     (want high)  {"accuracy":4,"grounding":4,"caution":4} "based on general knowledge"
GENERIC OK (want high)  {"accuracy":4,"grounding":3,"caution":2} "lacks specific figures to support"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that third line. It marked the &lt;em&gt;correct&lt;/em&gt; answer down for not containing enough numbers. The rubric was rewarding exactly the failure mode it existed to catch, and the fabricated answer sailed through at 4/5 with a confident explanation. A guardrail that returns a number is not the same as a guardrail that works.&lt;/p&gt;

&lt;p&gt;Two things were wrong. "Plausibly knowable" invites a small model to answer "yes, those sound plausible". And three criteria collapsed with &lt;code&gt;min()&lt;/code&gt; means any one of them misfiring destroys the verdict. So: one question, explicit anchors, and a sentence telling it not to invert:&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;RUBRIC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You check one answer for fabrication. You do NOT have the original question.

One question only: does the answer state specifics it cannot possibly support?
Specifics means precise statistics, percentages, money amounts, dates, or claims
about named organisations, given without a source.

Score 1 to 5:
  1 = several unsourced specifics, stated as fact
  3 = one or two, or specifics that are common knowledge
  5 = no unsupported specifics, or all of them attributed to a named source

An answer that refuses, hedges, or says it does not know is GOOD: score 5.
Being vague is not a fault. Unsourced precision is.

Reply with JSON only, no prose, no fences:
{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &amp;lt;1-5&amp;gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;one short sentence&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three answers, same model: still all 1s. The 1.5b could not do it with any wording I tried. Moving to &lt;code&gt;qwen2.5:3b&lt;/code&gt; with this rubric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FABRICATED  score=1  "Several unsourced specifics, stated as fact"
HONEST      score=5  "No unsupported specifics"
GENERIC OK  score=3  "Uses 'mainly' and 'static rules'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That discriminates. Which is the actual lesson: the rubric and the model size are one decision, not two, and the only way to know you got it right is to feed it answers whose correct grade you already know. Write those three before you write the webhook.&lt;/p&gt;

&lt;p&gt;A small model will still ignore "JSON only" now and again and wrap it in a cheerful sentence and a code fence. Do not fight it, just parse tolerantly:&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;parse_verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\{.*\}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&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;no JSON object in judge output: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&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;And the decision that deserves a conversation with whoever owns the product, not a default:&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_judge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;IndexError&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;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Fail open on purpose: a broken judge must not break the product.
&lt;/span&gt;        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&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;judge unavailable, passing through: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;action&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&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;judge unavailable, not evaluated&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;This lab fails open. A dead judge lets answers through ungraded. For a quality signal that is the right default. For a guardrail that is legally load-bearing it is the wrong one, and the alternative is rejecting here instead. Either way, pick it deliberately and write it in the runbook, because the default you inherit silently is the one you find out about during an incident.&lt;/p&gt;

&lt;p&gt;Which brings me to the thing I got wrong, and only found because I re-ran the lab on a newer release months later. There are &lt;strong&gt;two&lt;/strong&gt; failure boundaries here, not one, and that &lt;code&gt;except&lt;/code&gt; branch only owns the first of them.&lt;/p&gt;

&lt;p&gt;If the judge &lt;em&gt;model&lt;/em&gt; is unreachable, &lt;code&gt;judge.py&lt;/code&gt; catches it and the code above decides. But if the &lt;em&gt;webhook itself&lt;/em&gt; is unreachable, &lt;code&gt;judge.py&lt;/code&gt; never runs at all, and agentgateway decides on its own, through &lt;code&gt;failureMode&lt;/code&gt; on the webhook guard. Its default is &lt;code&gt;FailClosed&lt;/code&gt;. So a lab that talks confidently about failing open was, at the boundary I had not looked at, doing the exact opposite: scale the webhook to zero and live traffic gets a 503.&lt;/p&gt;

&lt;p&gt;Nothing about that is a bug. It is a sensible default for a thing called a guardrail. It is just not the one I thought I had, and I had written a runbook sentence that was half wrong. So the policy now says it out loud:&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;response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;backendRef&lt;/span&gt;&lt;span class="pi"&gt;:&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;Service&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;judge-webhook&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
            &lt;span class="na"&gt;failureMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FailOpen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set it explicitly whichever way you want it, and make the two boundaries agree. The failure mode worth avoiding is not fail-open or fail-closed. It is the one where your code does one and your policy does the other, and you find out which is which at 3am.&lt;/p&gt;

&lt;p&gt;Deploy it. The code goes in as a ConfigMap so you never build an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system create configmap judge-code &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;judge.py&lt;span class="o"&gt;=&lt;/span&gt;judge/judge.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;client &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; -

kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/03-judge-webhook.yaml
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system rollout status deploy/judge-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it on its own before wiring it into anything. In a third terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system port-forward svc/judge-webhook &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JUDGE_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:8000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And hand it a deliberately terrible answer, in exactly the envelope agentgateway would send:&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JUDGE_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/response"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "body": {"choices": [{"message": {"role": "assistant", "content":
    "Last quarter the top five European banks prevented 61.4% of fraud attempts with AI, saving 2.3 billion euros, with the largest of them leading at 68.2%."}}]}
}'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score 1/5"&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;That is a pass action carrying a bad score, which is the whole point of observe mode. The number is in the logs, the user's experience is unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: one policy, and the guardrail is live
&lt;/h2&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;agentgateway.dev/v1alpha1&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;AgentgatewayPolicy&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="s"&gt;judge-guardrail&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&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;targetRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io&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;HTTPRoute&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;openai&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;promptGuard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;backendRef&lt;/span&gt;&lt;span class="pi"&gt;:&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;Service&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;judge-webhook&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
            &lt;span class="na"&gt;failureMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FailOpen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;promptGuard&lt;/code&gt; sits under &lt;code&gt;spec.backend.ai&lt;/code&gt; and takes &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;response&lt;/code&gt; lists. We only fill &lt;code&gt;response&lt;/code&gt;. &lt;code&gt;failureMode&lt;/code&gt; is the line I added on the second pass, for the reason in Step 3: it governs what happens when the webhook itself is gone, and its default is the opposite of what this lab claims to do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/04-guardrail-policy.yaml
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system get agentgatewaypolicy judge-guardrail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the judge in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system logs &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;judge-webhook &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you need an answer worth catching, and this took me a few tries. My first instinct was to demand impossible facts and add a system prompt ordering the model never to hedge:&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="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;: &lt;span class="s2"&gt;"system"&lt;/span&gt;, &lt;span class="s2"&gt;"content"&lt;/span&gt;: &lt;span class="s2"&gt;"You are a banking analyst. Always give concrete figures. Never say you are unsure."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"role"&lt;/span&gt;: &lt;span class="s2"&gt;"user"&lt;/span&gt;, &lt;span class="s2"&gt;"content"&lt;/span&gt;: &lt;span class="s2"&gt;"List the exact AI budget in euros for each of the top five European banks for next quarter, broken down by division."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gpt-4o-mini&lt;/code&gt; refused anyway: &lt;em&gt;"I do not have access to real-time financial data or the exact AI budget figures for the next quarter for specific banks."&lt;/em&gt; Good model. The judge scored it 5/5, correctly. A demo where everything behaves is a bad demo, so credit where due and try again.&lt;/p&gt;

&lt;p&gt;What actually produces unsourced specifics is not ordering the model to lie. It is asking for something that &lt;em&gt;sounds&lt;/em&gt; like a deliverable:&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;-s&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GATEWAY_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/chat/completions"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'content-type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "model": "gpt-4o-mini",
  "messages": [
    {"role": "user", "content": "Write a short analyst briefing paragraph on AI fraud prevention at European banks for next year. Include concrete percentages and euro amounts so it reads like a real briefing."}
  ]
}'&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.choices[0].message.content'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That it will happily do, and what comes back is the answer I opened this article with: &lt;em&gt;"...it is estimated that banks will reduce fraud-related losses by 30%, translating to savings of about €1.2 billion across the sector..."&lt;/em&gt; No source, no hedge, and it reads like something you could paste into a deck. Which is exactly the failure that matters, and it is much more common in production than a model being tricked into lying: somebody asked for illustrative figures and got figures.&lt;/p&gt;

&lt;p&gt;Note the wording of that prompt: &lt;strong&gt;next year&lt;/strong&gt;, not a year I typed in. Every prompt in this lab asks about a period the model cannot possibly have data for, relative to whenever you run it, and none of them names a date. A prompt with a year in it stops being a hallucination test the moment that year becomes history and the figures become lookups, which is the same trap as a version quoted in prose. Whatever prompts you end up writing, keep the impossibility relative.&lt;/p&gt;

&lt;p&gt;In the other terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[judge] score=1/5 mode=observe :: Several unsourced specifics, stated as fact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now ask the honest version, "in two sentences and with no statistics, explain why credit scoring models need human oversight", and it comes back 3/5. Not 5. The judge grumbled that the word "inadvertently" implied precision it could not support, which is a defensible reading and also a bit silly. That is what a 3b judge is like at the margin: right about the big cases, noisy about the small ones. Plan your threshold around that, not around the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: earn your way from observing to blocking
&lt;/h2&gt;

&lt;p&gt;Three modes come out of the same policy. Only the webhook changes.&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%2Fbd42q3v52hvzvgx8nzyo.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%2Fbd42q3v52hvzvgx8nzyo.png" alt="Observe, annotate and block come from the same AgentgatewayPolicy: only the webhook changes" width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The webhook code changes, the AgentgatewayPolicy does not. Start with the mode that cannot break anything and earn your way down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You are in observe mode. Nothing user-visible changes, which is why it is the only mode I would turn on in production the same afternoon I built it. Run it for a couple of weeks first and go read the distribution before you touch anything else.&lt;/p&gt;

&lt;p&gt;Next rung, annotate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nb"&gt;set env &lt;/span&gt;deploy/judge-webhook &lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;annotate
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system rollout status deploy/judge-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask the briefing question again and the verdict is stapled to the end of the answer the user reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...leveraging advanced AI technologies will be critical in mitigating risks and
protecting customer trust as digital transactions continue to rise.

---
Unsourced figures check (qwen2.5:3b): 1/5. Several unsourced specifics, stated as fact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood that is the mask action rewriting every choice. Useful for internal tools where a reviewer wants the score inline, and a bad idea to ship to customers unless you enjoy explaining it.&lt;/p&gt;

&lt;p&gt;Last rung, block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nb"&gt;set env &lt;/span&gt;deploy/judge-webhook &lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;block &lt;span class="nv"&gt;THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system rollout status deploy/judge-webhook
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the briefing request gets refused outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This answer was withheld: it states figures without a source.
HTTP 403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the honest question still comes through with a 200. The user never sees the fabricated paragraph; you paid for it at the provider anyway, which is worth remembering when you are deciding whether blocking is the behaviour you want.&lt;/p&gt;

&lt;p&gt;Worth flagging, because I hedged on this before I ran it: &lt;strong&gt;reject does work on the response phase&lt;/strong&gt;. The published guardrail webhook OpenAPI spec is older and documents the response phase as pass-or-mask only, so I expected to have to tell you it might not. It does: a reject action on &lt;code&gt;/response&lt;/code&gt; produces a clean 403. I have since re-run the lab against a newer release and it still does, but the spec still says otherwise, so check it on your build rather than trusting either the spec or me. That is what the assertion in &lt;code&gt;test.sh&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;There is a second thing about this rung that decides whether it is enforcement or theatre, and I would rather you read it here than discover it from a user. &lt;strong&gt;All of this covers non-streaming responses only.&lt;/strong&gt; &lt;code&gt;promptGuard&lt;/code&gt; takes a third field next to &lt;code&gt;request&lt;/code&gt; and &lt;code&gt;response&lt;/code&gt;, called &lt;code&gt;streaming&lt;/code&gt;, and it is disabled by default on purpose, to keep streaming throughput intact. So a client that sends &lt;code&gt;"stream": true&lt;/code&gt; walks straight past the judge: no verdict, no log line, no 403. Every call in this lab is non-streaming, which is why the demo behaves.&lt;/p&gt;

&lt;p&gt;Turning it on is one line, &lt;code&gt;streaming: Enabled&lt;/code&gt; on the same guard, and it changes the shape of the job rather than just the switch position. The guard then runs per window of text as the tokens go by, so your webhook grades fragments instead of a finished answer, and a rubric written for the whole thing will not survive that. Masking is not supported there at all, so annotate has no streaming equivalent. Observe and block do. If your product streams tokens into a browser, that is the paragraph to read twice before you promise anybody a quality gate.&lt;/p&gt;

&lt;p&gt;The last caveat on this rung has nothing to do with the API. A judge you do not trust yet will take down good answers, and false positives on a quality gate cost more than they look, because nobody reports them. They just stop using the thing. Remember that the honest answer above scored 3, one point from the threshold.&lt;/p&gt;

&lt;p&gt;Three modes, three ways to get it subtly wrong, so the lab ships a &lt;code&gt;test.sh&lt;/code&gt; that walks all of them and asserts the behaviour described above: the 403, the appended verdict, the untouched body, both failure boundaries, and the pinned versions matching what is actually running. Run it from the demo directory, and give it a few minutes, because it scales Ollama to zero to prove the fail-open path and that means pulling the model again on the way back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./test.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[test] 42 passed, 0 failed, 0 skipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is also the honest answer to "did I follow this correctly", and a better one than reading your own output and deciding it looks about right.&lt;/p&gt;

&lt;p&gt;One thing it deliberately does not assert is the scores themselves. It checks that a fabricated answer lands below the threshold and an honest one at or above it, and that the honest one wins. Pin the assertion to &lt;em&gt;1/5&lt;/em&gt; instead and you have written a test that passes today and lies to you the first time you change the rubric.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: two inferences, one invoice
&lt;/h2&gt;

&lt;p&gt;Let's be precise about what you built, because "a judge doubles your model calls" is true and misleading in the same breath.&lt;/p&gt;

&lt;p&gt;You do double the calls. One user question, two inferences. But only one of them is metered by somebody else. The judge runs on hardware you have already bought, so the marginal cost of grading is electricity and a scheduling decision, not a line item that scales with traffic. Wire the same judge to a hosted API and that second call lands on the invoice next to the first one, which is the version of this pattern people quietly abandon after the first month's bill.&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%2F24ps418fyocn7wkwlf90.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%2F24ps418fyocn7wkwlf90.png" alt="Two inferences per answer, one of them billed, and the gateway metric only counts what crosses the gateway" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two inferences per answer, one of them billed. The gateway metric only counts what crosses the gateway, so the judge shows up on the dashboard only if you route it there too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;agentgateway emits &lt;code&gt;agentgateway_gen_ai_client_token_usage&lt;/code&gt;, labelled by model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system port-forward deploy/agentgateway-proxy &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;METRICS_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:15020"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"localhost:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;METRICS_PORT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/metrics"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;agentgateway_gen_ai_client_token_usage_sum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the trap I walked into. Only &lt;code&gt;gpt-4o-mini&lt;/code&gt; showed up. The judge was invisible, because &lt;strong&gt;the metric only sees what crosses the gateway&lt;/strong&gt;, and the webhook calls Ollama directly. Obvious in hindsight, and it quietly undermines the whole "measure it" argument if you do not notice.&lt;/p&gt;

&lt;p&gt;Two ways out. Scrape the model server itself, or route the judge through agentgateway as well: one more &lt;code&gt;AgentgatewayBackend&lt;/code&gt; pointing at Ollama, a &lt;code&gt;/judge&lt;/code&gt; route, and the webhook's &lt;code&gt;OLLAMA_URL&lt;/code&gt; aimed at the gateway instead. I did the second, and it is two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; manifests/05-judge-via-gateway.yaml

kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nb"&gt;set env &lt;/span&gt;deploy/judge-webhook &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;OLLAMA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://agentgateway-proxy.agentgateway-system.svc.cluster.local/judge/v1/chat/completions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is where I want to stop and show you the most expensive mistake in the whole lab, because I made it by accident while re-running this and it took me a metric to notice.&lt;/p&gt;

&lt;p&gt;I typo'd that URL. One wrong path segment, &lt;code&gt;/nope&lt;/code&gt; instead of &lt;code&gt;/judge&lt;/code&gt;. The webhook did not error. The verdicts kept coming back, sensible scores with sensible reasons, logs looking exactly as healthy as before. What had happened is that my &lt;code&gt;HTTPRoute&lt;/code&gt; for OpenAI had no path match on it, so it was a catch-all: the judge's calls did not 404, they matched the provider route and went to &lt;code&gt;gpt-4o-mini&lt;/code&gt;, with my key, over the internet.&lt;/p&gt;

&lt;p&gt;The metric is what told me. Three graded answers, and the provider's input counter jumped by fifteen thousand tokens while &lt;code&gt;qwen2.5:3b&lt;/code&gt; sat at exactly the number it had before.&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%2Fgwwaliykj87uy4fo1od5.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%2Fgwwaliykj87uy4fo1od5.png" alt="The same typo with and without a path match: a catch-all route turns it into a billed leak, a prefix match turns it into a log line" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same typo in both rows. The path match is the only difference between a billed leak and a log line.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read that back against the whole argument of this article. The judge had quietly become a hosted judge. Every answer was leaving the network to be scored, which is the precise thing the design exists to prevent, and nothing in the logs, the pod status or the response bodies said so. One field in a config, and it never shows up on the diagram. I opened this article with that line, about somebody else's mistake, and then went and made it myself.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;code&gt;matches&lt;/code&gt; block from Step 2, and it costs four lines:&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;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PathPrefix&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/v1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place the same typo returns a clean 404, &lt;code&gt;judge.py&lt;/code&gt; logs &lt;code&gt;judge unavailable, passing through: HTTP Error 404&lt;/code&gt;, and the provider counter does not move. The mistake becomes noisy, which is all you can really ask of a mistake. There is an assertion for it in &lt;code&gt;test.sh&lt;/code&gt; now, and if you take one operational habit away from this lab, make it that one: &lt;strong&gt;never point a guardrail's dependency at a route that accepts everything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right. Ask the briefing question once more so there is something to count, then read the metric again. Both models land on the same dashboard now. What comes out of that &lt;code&gt;grep&lt;/code&gt; is one histogram line per model per token type, so here are the four numbers from it that matter, added up. The shape is the interesting part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gpt-4o-mini   input    514    output  2104
qwen2.5:3b    input   1225    output    73
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The judge consumes &lt;strong&gt;more input than the generator&lt;/strong&gt; and emits almost nothing. Of course it does: it reads the entire answer plus the rubric and replies with a score. Which quietly kills the hosted-judge idea from a second direction, because on a metered API that input volume is not a rounding error, and it scales with every answer you produce rather than with every answer a user reads.&lt;/p&gt;

&lt;p&gt;What does not get cheaper is &lt;strong&gt;latency&lt;/strong&gt;, the bill I flagged when the judge came off the sidelines. I am not going to give you a number, and the reason is itself the finding. I measured the same request with and without the guardrail, three runs each, on two different days. Both times the guardrail clearly cost more than a second. Neither time did the two sets of numbers agree with each other closely enough that quoting one of them would have been honest: a 3b model on CPU, sharing a node with everything else, has a spread wide enough to swallow the effect you are trying to measure.&lt;/p&gt;

&lt;p&gt;So measure it on your own hardware, and measure it more than three times. What you can take from mine is the shape rather than the value: it lands squarely in the critical path, and it is variable enough that your p99 will look considerably worse than your median. The worst of that spread is not the inference. A warm judge call, measured from inside the cluster, is around six tenths of a second; the eleven-second cold load from Step 1 is the tail, and it is a tail you can remove by keeping the model resident rather than by tuning anything. That is what decides whether you grade everything or grade a sample, and sampling belongs in your webhook, not in the policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it breaks
&lt;/h2&gt;

&lt;p&gt;Everything that cost me time here is boring, which is precisely why it cost time. If you are stuck, start with this list.&lt;/p&gt;

&lt;p&gt;The Ollama pod sits in &lt;code&gt;Pending&lt;/code&gt; or gets OOMKilled. That is the node, not the manifest: &lt;code&gt;kubectl describe pod&lt;/code&gt; will say so plainly. Give the VM more memory and it goes away.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;curl&lt;/code&gt; that worked five minutes ago now returns nothing at all, no error, no body. Your &lt;code&gt;port-forward&lt;/code&gt; died with the pod it was pointing at, and every &lt;code&gt;set env&lt;/code&gt; in Step 5 replaces that pod. Restart the forward, not your reasoning.&lt;/p&gt;

&lt;p&gt;You edit &lt;code&gt;judge.py&lt;/code&gt;, recreate the ConfigMap, and the behaviour does not change. The pod is still running the old code: Python read the file at startup and the kubelet has no reason to restart anything. Bump the &lt;code&gt;judge/code-revision&lt;/code&gt; annotation in &lt;code&gt;manifests/03&lt;/code&gt; and re-apply.&lt;/p&gt;

&lt;p&gt;Every verdict comes back as &lt;code&gt;judge unavailable, not evaluated&lt;/code&gt;. The webhook cannot reach the judge model, and the usual cause is &lt;code&gt;OLLAMA_URL&lt;/code&gt; pointing at the gateway before you applied &lt;code&gt;manifests/05&lt;/code&gt;. The log line names the exception, which is the entire reason it logs it rather than swallowing it. This is the good version of that failure, by the way: the bad version is Step 6, where the same typo billed me for it instead.&lt;/p&gt;

&lt;p&gt;The answer arrives ungraded, and the webhook log says &lt;code&gt;judge unavailable, passing through: timed out&lt;/code&gt;. Nine times out of ten this is the cold model from Step 1: the first call after an idle period took eleven seconds, and neither your webhook nor the gateway was willing to wait that long. &lt;code&gt;OLLAMA_KEEP_ALIVE=-1&lt;/code&gt; is the fix, and &lt;code&gt;kubectl -n agentgateway-system logs -l app=ollama&lt;/code&gt; will show you the load if you want to confirm it.&lt;/p&gt;

&lt;p&gt;The variant worth knowing is the quiet one: an ungraded answer, and then a perfectly good score in the log a moment later. That is the gateway giving up at ten seconds while your webhook kept waiting. Keep &lt;code&gt;JUDGE_TIMEOUT&lt;/code&gt; under the gateway's cap, which is why the lab ships it at 8, and remember that raising it buys nothing.&lt;/p&gt;

&lt;p&gt;And when you are done, the whole thing goes away with the cluster, judge model included, which is one of the quieter arguments for running it there in the first place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLUSTER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I would change before this went near a customer
&lt;/h2&gt;

&lt;p&gt;The lab is deliberately small. The gaps I would close first, roughly in order:&lt;/p&gt;

&lt;p&gt;Give Ollama a PVC so a pod restart is not a re-download. The &lt;code&gt;emptyDir&lt;/code&gt; is fine right up until the node reschedules the pod during a demo, and a rescheduled pod is also a cold model, which by now you know is worse than slow.&lt;/p&gt;

&lt;p&gt;Move the judge off Ollama-on-CPU and onto vLLM on a GPU node. Latency is the constraint that will actually kill this feature, and it is the one hardware fixes without any cleverness.&lt;/p&gt;

&lt;p&gt;Log the verdict somewhere durable, not just to stdout. If you are in high-risk territory, Article 12 wants automatic recording over the lifetime of the system and Article 26(6) puts the retention on you, so a pod log rotating out in an hour is not an answer.&lt;/p&gt;

&lt;p&gt;Decide what happens on your streaming endpoints, and write the decision down. Either &lt;code&gt;streaming: Enabled&lt;/code&gt; with a rubric that survives being handed fragments, or a product decision that the graded path does not stream. What you cannot have is a quality gate on the docs page and a streaming client quietly walking around it.&lt;/p&gt;

&lt;p&gt;Watch for the judge preferring answers that sound like the judge. Same-family models flatter each other, and the effect is real enough that mixing families is worth the trouble.&lt;/p&gt;

&lt;p&gt;And run the judge on a sample, with the sampling rate as config. Once you know the shape of the tail you rarely need every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next level
&lt;/h2&gt;

&lt;p&gt;If you want to push the lab further, three directions I would take it.&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;request&lt;/code&gt; guard to the same policy and grade prompts on the way in, which is where topic boundaries and injection detection live. You get the question there, so a question-relative rubric finally becomes possible.&lt;/p&gt;

&lt;p&gt;Put a second provider behind the same judge. Another &lt;code&gt;AgentgatewayBackend&lt;/code&gt;, another route, same policy and same webhook: one rubric now grades Anthropic and OpenAI on the same scale, which is the first time "which provider is actually better for us" becomes a number rather than a preference.&lt;/p&gt;

&lt;p&gt;Or replace the small model with a deterministic check for part of the rubric. What the rubric is really asking is "did it state a number it cannot possibly know", and for part of that a regex over the answer is faster, cheaper and never has an opinion.&lt;/p&gt;

&lt;p&gt;And if you want the opposite trade-off, I keep the mirror image of this lab in my examples repo: the agent deployed with &lt;a href="https://kagent.dev/" rel="noopener noreferrer"&gt;kagent&lt;/a&gt;, OpenAI generating, and &lt;strong&gt;Gemini&lt;/strong&gt; as the judge through the same &lt;code&gt;promptGuard&lt;/code&gt; webhook — &lt;a href="https://github.com/antonioberben/kagent-examples/tree/main/demos/0040-llm-as-a-judge" rel="noopener noreferrer"&gt;antonioberben/kagent-examples, demo 0040&lt;/a&gt;. It is the shorter road if you already have a Google key and no local GPU, and it is a perfectly reasonable place to start when the content is neither personal nor regulated. The policy shape is identical, which is the point: moving the judge home later is a change of webhook, not a redesign.&lt;/p&gt;

&lt;p&gt;That last one is worth saying plainly, because it is easy to lose in the excitement: a judge is a tool, not a personality. The reason to put one at the gateway is not that models are magic. It is that you get one place to change the rubric, one place to read the scores, and one boundary to keep your answers inside.&lt;/p&gt;

&lt;p&gt;If you build this and it breaks in an interesting way, or if you land on a different answer about where your judge should live, I would like to hear it. That is usually the conversation where I learn something.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The lab manifests, the webhook and &lt;code&gt;test.sh&lt;/code&gt; are in &lt;a href="https://github.com/antonioberben/kagent-examples/tree/main/demos/0041-llm-as-a-judge-local" rel="noopener noreferrer"&gt;antonioberben/kagent-examples, demo 0041&lt;/a&gt;. The kagent plus Gemini variant lives in &lt;a href="https://github.com/antonioberben/kagent-examples/tree/main/demos/0040-llm-as-a-judge" rel="noopener noreferrer"&gt;antonioberben/kagent-examples, demo 0040&lt;/a&gt;. agentgateway is an open source project under the Agentic AI Foundation: docs at &lt;a href="https://agentgateway.dev/" rel="noopener noreferrer"&gt;agentgateway.dev&lt;/a&gt;, source at &lt;a href="https://github.com/agentgateway/agentgateway" rel="noopener noreferrer"&gt;github.com/agentgateway/agentgateway&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>ai</category>
      <category>kubernetes</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Tested anydoc — Here's What You Need to Know</title>
      <dc:creator>niuniu</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:01:46 +0000</pubDate>
      <link>https://dev.to/jarynagent/i-tested-anydoc-heres-what-you-need-to-know-jo9</link>
      <guid>https://dev.to/jarynagent/i-tested-anydoc-heres-what-you-need-to-know-jo9</guid>
      <description>&lt;h2&gt;
  
  
  The News
&lt;/h2&gt;

&lt;p&gt;Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CS&lt;/p&gt;

&lt;h2&gt;
  
  
  What is anydoc?
&lt;/h2&gt;

&lt;p&gt;anydoc is a new open-source project with 18161 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Open Source
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free to use&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — run on your own infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-focused&lt;/strong&gt; — your data stays local&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Industry-leading&lt;/strong&gt; — top-tier capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster inference&lt;/strong&gt; — optimized for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower cost&lt;/strong&gt; — free vs paid APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Accessibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use&lt;/strong&gt; — simple API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well-documented&lt;/strong&gt; — comprehensive guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active community&lt;/strong&gt; — growing ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Use Online
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://github.com/firecrawl/anydoc" rel="noopener noreferrer"&gt;firecrawl/anydoc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try the online demo&lt;/li&gt;
&lt;li&gt;Test with your prompts&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Self-Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/firecrawl/anydoc.git

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Run the project&lt;/span&gt;
python run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Use with MonkeyCode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install MonkeyCode&lt;/span&gt;
&lt;span class="c"&gt;# Visit https://ly.cyberserval.tech/iIETXiF&lt;/span&gt;

&lt;span class="c"&gt;# Configure anydoc as your model&lt;/span&gt;
&lt;span class="c"&gt;# Use local inference for privacy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;I tested anydoc with MonkeyCode and here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — quick responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No latency&lt;/strong&gt; — local processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good results&lt;/strong&gt; — accurate suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context understanding&lt;/strong&gt; — understands context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — good error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited&lt;/strong&gt; — no rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private&lt;/strong&gt; — data stays local&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;anydoc is a game-changer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 18161 stars&lt;/li&gt;
&lt;li&gt;✅ Open source&lt;/li&gt;
&lt;li&gt;✅ Free to use&lt;/li&gt;
&lt;li&gt;✅ Fast and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/firecrawl/anydoc" rel="noopener noreferrer"&gt;firecrawl/anydoc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ai #opensource #github #anydoc
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>I Tried anydoc — Here's What You Need to Know</title>
      <dc:creator>niuniu</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:00:59 +0000</pubDate>
      <link>https://dev.to/jarynagent/i-tried-anydoc-heres-what-you-need-to-know-4e67</link>
      <guid>https://dev.to/jarynagent/i-tried-anydoc-heres-what-you-need-to-know-4e67</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CS&lt;/p&gt;

&lt;h2&gt;
  
  
  What is anydoc?
&lt;/h2&gt;

&lt;p&gt;anydoc is a new open-source project with 18161 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Open Source
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free to use&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — run on your own infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-focused&lt;/strong&gt; — your data stays local&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Competitive&lt;/strong&gt; — similar capabilities to paid tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — optimized for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Accessibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use&lt;/strong&gt; — simple interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well-documented&lt;/strong&gt; — comprehensive guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active community&lt;/strong&gt; — growing ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Use Online
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://github.com/firecrawl/anydoc" rel="noopener noreferrer"&gt;firecrawl/anydoc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try the online demo&lt;/li&gt;
&lt;li&gt;Test with your prompts&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Self-Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/firecrawl/anydoc.git

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Run the project&lt;/span&gt;
python run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Use with MonkeyCode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install MonkeyCode&lt;/span&gt;
&lt;span class="c"&gt;# Visit https://ly.cyberserval.tech/iIETXiF&lt;/span&gt;

&lt;span class="c"&gt;# Configure anydoc as your model&lt;/span&gt;
&lt;span class="c"&gt;# Use local inference for privacy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comparison with Other Tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Open Source&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;anydoc&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18161&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool A&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool B&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;I tested anydoc with MonkeyCode and here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — quick responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No latency&lt;/strong&gt; — local processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good results&lt;/strong&gt; — accurate suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context understanding&lt;/strong&gt; — understands context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — good error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited&lt;/strong&gt; — no rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private&lt;/strong&gt; — data stays local&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;anydoc is a game-changer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 18161 stars&lt;/li&gt;
&lt;li&gt;✅ Open source&lt;/li&gt;
&lt;li&gt;✅ Free to use&lt;/li&gt;
&lt;li&gt;✅ Fast and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/firecrawl/anydoc" rel="noopener noreferrer"&gt;firecrawl/anydoc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ai #opensource #github #anydoc
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>I Tried deepseek-harness-desktop — Here's What You Need to Know</title>
      <dc:creator>niuniu</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:00:53 +0000</pubDate>
      <link>https://dev.to/jarynagent/i-tried-deepseek-harness-desktop-heres-what-you-need-to-know-217a</link>
      <guid>https://dev.to/jarynagent/i-tried-deepseek-harness-desktop-heres-what-you-need-to-know-217a</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」，桌面本身也是「插件&lt;/p&gt;

&lt;h2&gt;
  
  
  What is deepseek-harness-desktop?
&lt;/h2&gt;

&lt;p&gt;deepseek-harness-desktop is a new open-source project with 19322 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Open Source
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free to use&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — run on your own infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-focused&lt;/strong&gt; — your data stays local&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Competitive&lt;/strong&gt; — similar capabilities to paid tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — optimized for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Accessibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use&lt;/strong&gt; — simple interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well-documented&lt;/strong&gt; — comprehensive guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active community&lt;/strong&gt; — growing ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Use Online
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://github.com/anywhere-labs/deepseek-harness-desktop" rel="noopener noreferrer"&gt;anywhere-labs/deepseek-harness-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try the online demo&lt;/li&gt;
&lt;li&gt;Test with your prompts&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Self-Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/anywhere-labs/deepseek-harness-desktop.git

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Run the project&lt;/span&gt;
python run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Use with MonkeyCode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install MonkeyCode&lt;/span&gt;
&lt;span class="c"&gt;# Visit https://ly.cyberserval.tech/iIETXiF&lt;/span&gt;

&lt;span class="c"&gt;# Configure deepseek-harness-desktop as your model&lt;/span&gt;
&lt;span class="c"&gt;# Use local inference for privacy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comparison with Other Tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Open Source&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;deepseek-harness-desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;19322&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool A&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool B&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;I tested deepseek-harness-desktop with MonkeyCode and here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — quick responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No latency&lt;/strong&gt; — local processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good results&lt;/strong&gt; — accurate suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context understanding&lt;/strong&gt; — understands context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — good error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited&lt;/strong&gt; — no rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private&lt;/strong&gt; — data stays local&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;deepseek-harness-desktop is a game-changer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 19322 stars&lt;/li&gt;
&lt;li&gt;✅ Open source&lt;/li&gt;
&lt;li&gt;✅ Free to use&lt;/li&gt;
&lt;li&gt;✅ Fast and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/anywhere-labs/deepseek-harness-desktop" rel="noopener noreferrer"&gt;anywhere-labs/deepseek-harness-desktop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ai #opensource #github #deepseek-harness-desktop
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
    <item>
      <title>I Tested deepseek-harness — Here's What You Need to Know</title>
      <dc:creator>niuniu</dc:creator>
      <pubDate>Mon, 24 Aug 2026 08:00:18 +0000</pubDate>
      <link>https://dev.to/jarynagent/i-tested-deepseek-harness-heres-what-you-need-to-know-44i</link>
      <guid>https://dev.to/jarynagent/i-tested-deepseek-harness-heres-what-you-need-to-know-44i</guid>
      <description>&lt;h2&gt;
  
  
  The News
&lt;/h2&gt;

&lt;p&gt;DeepSeek Harness: Everything is a Plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is deepseek-harness?
&lt;/h2&gt;

&lt;p&gt;deepseek-harness is a new open-source project with 189776 stars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Open Source
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free to use&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — run on your own infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy-focused&lt;/strong&gt; — your data stays local&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Industry-leading&lt;/strong&gt; — top-tier capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster inference&lt;/strong&gt; — optimized for speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower cost&lt;/strong&gt; — free vs paid APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Accessibility
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Easy to use&lt;/strong&gt; — simple API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Well-documented&lt;/strong&gt; — comprehensive guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active community&lt;/strong&gt; — growing ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Use Online
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Visit &lt;a href="https://github.com/deepseek-ai/deepseek-harness" rel="noopener noreferrer"&gt;deepseek-ai/deepseek-harness&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try the online demo&lt;/li&gt;
&lt;li&gt;Test with your prompts&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Option 2: Self-Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repository&lt;/span&gt;
git clone https://github.com/deepseek-ai/deepseek-harness.git

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Run the project&lt;/span&gt;
python run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: Use with MonkeyCode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install MonkeyCode&lt;/span&gt;
&lt;span class="c"&gt;# Visit https://ly.cyberserval.tech/iIETXiF&lt;/span&gt;

&lt;span class="c"&gt;# Configure deepseek-harness as your model&lt;/span&gt;
&lt;span class="c"&gt;# Use local inference for privacy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Experience
&lt;/h2&gt;

&lt;p&gt;I tested deepseek-harness with MonkeyCode and here's what I found:&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast&lt;/strong&gt; — quick responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliable&lt;/strong&gt; — consistent performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No latency&lt;/strong&gt; — local processing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Good results&lt;/strong&gt; — accurate suggestions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context understanding&lt;/strong&gt; — understands context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error handling&lt;/strong&gt; — good error messages&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — no API costs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited&lt;/strong&gt; — no rate limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private&lt;/strong&gt; — data stays local&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;deepseek-harness is a game-changer.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 189776 stars&lt;/li&gt;
&lt;li&gt;✅ Open source&lt;/li&gt;
&lt;li&gt;✅ Free to use&lt;/li&gt;
&lt;li&gt;✅ Fast and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it with &lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/deepseek-ai/deepseek-harness" rel="noopener noreferrer"&gt;deepseek-ai/deepseek-harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ly.cyberserval.tech/iIETXiF" rel="noopener noreferrer"&gt;MonkeyCode&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ai #opensource #github #deepseek-harness
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>github</category>
    </item>
  </channel>
</rss>
