diff --git a/404.html b/404.html index 2c3a856..86c404d 100644 --- a/404.html +++ b/404.html @@ -4,4 +4,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/about/index.html b/about/index.html index 848b460..1c207e9 100644 --- a/about/index.html +++ b/about/index.html @@ -13,4 +13,4 @@ My work focuses on Infrastructure Performance and Customer Engineering, specific 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/authors/index.html b/authors/index.html index d59dee8..894d2e8 100644 --- a/authors/index.html +++ b/authors/index.html @@ -4,4 +4,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/categories/index.html b/categories/index.html index 75871f9..ad45280 100644 --- a/categories/index.html +++ b/categories/index.html @@ -4,4 +4,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/index.html b/index.html index 7b39e1f..9f0bef0 100644 --- a/index.html +++ b/index.html @@ -1,8 +1,8 @@ -Eric X. Liu's Personal Page
avatar

Eric X. Liu

  • +
\ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/index.xml b/index.xml index 365daad..dd7f01d 100644 --- a/index.xml +++ b/index.xml @@ -1,4 +1,13 @@ -Eric X. Liu's Personal Pagehttps://ericxliu.me/Recent content on Eric X. Liu's Personal PageHugoenThu, 22 Jan 2026 06:48:07 +0000Hacking a Chinese Car Stereo to fulfill my Knight Rider dreamshttps://ericxliu.me/posts/vibe-coding-from-the-jeep/Wed, 21 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/vibe-coding-from-the-jeep/<p>&ldquo;Vibe coding&rdquo; has become my latest obsession. It&rsquo;s that flow state where the tools disappear, and you&rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&rsquo;ve been trying to answer a childhood dream.</p> +Eric X. Liu's Personal Pagehttps://ericxliu.me/Recent content on Eric X. Liu's Personal PageHugoenWed, 04 Feb 2026 06:18:45 +0000Deployment Lessons and My Take on Self-Hosting OpenClawhttps://ericxliu.me/posts/blog-draft/Tue, 03 Feb 2026 00:00:00 +0000https://ericxliu.me/posts/blog-draft/<p>Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.</p> +<p>Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.</p> +<h2 id="layer-1-the-environment--breaking-the-sandbox"> + Layer 1: The Environment – Breaking the Sandbox + <a class="heading-link" href="#layer-1-the-environment--breaking-the-sandbox"> + <i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"></i> + <span class="sr-only">Link to heading</span> + </a> +</h2> +<p>To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.</p>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreamshttps://ericxliu.me/posts/vibe-coding-from-the-jeep/Wed, 21 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/vibe-coding-from-the-jeep/<p>&ldquo;Vibe coding&rdquo; has become my latest obsession. It&rsquo;s that flow state where the tools disappear, and you&rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&rsquo;ve been trying to answer a childhood dream.</p> <p>Growing up in China before the internet age, my window to the outside world was CCTV-6. Along with <em>Baywatch</em>, one of the first American TV shows I ever watched was <em>Knight Rider</em>. I don&rsquo;t remember the exact plot lines, but the core concept stuck with me forever: KITT. A car that could talk, think, and do things for you.</p>How I Built a Blog Agent that Writes About Itselfhttps://ericxliu.me/posts/reverse-engineering-antigravity-ide/Fri, 16 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/reverse-engineering-antigravity-ide/<p>I&rsquo;ve been spending a lot of time &ldquo;vibe coding&rdquo; in the Antigravity IDE lately. It&rsquo;s an incredible flow state—intense, iterative, and fast. But it has a major flaw: the context is ephemeral. Once the session is over, that rich history of decisions, wrong turns, and &ldquo;aha!&rdquo; moments is locked away in an opaque, internal format.</p> <p>I wanted to capture that value. I wanted a system that could take my chaotic coding sessions and distill them into structured, technical blog posts (like the one you&rsquo;re reading right now).</p>Why I Downgraded Magisk to Root My Pixel 2 XLhttps://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/Wed, 07 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/<p>For the past few weeks, I&rsquo;ve been stuck in a stalemate with my EcoFlow Bluetooth Protocol Reverse Engineering Project. I have the hci snoop logs, I have the decompiled APK, and I have a strong suspicion about where the authentication logic is hiding. But suspicion isn&rsquo;t proof.</p> <p>Static analysis has its limits. I found the &ldquo;smoking gun&rdquo; function—a native method responsible for encrypting the login payload—but understanding <em>how</em> it constructs that payload within a strict 13-byte limit purely from assembly (ARM64) was proving to be a headache.</p>Why Your "Resilient" Homelab is Slower Than a Raspberry Pihttps://ericxliu.me/posts/debugging-authentik-performance/Fri, 02 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/debugging-authentik-performance/<p>In the world of self-hosting, there are many metrics for success: 99.9% uptime, sub-second latency, or a perfect GitOps pipeline. But for those of us running &ldquo;production&rdquo; at home, there is only one metric that truly matters: <strong>The Wife Acceptance Factor (WAF)</strong>.</p> diff --git a/posts/benchmarking-llms-on-jetson-orin-nano/index.html b/posts/benchmarking-llms-on-jetson-orin-nano/index.html index 52105fb..438a3bf 100644 --- a/posts/benchmarking-llms-on-jetson-orin-nano/index.html +++ b/posts/benchmarking-llms-on-jetson-orin-nano/index.html @@ -39,7 +39,7 @@ After running 66 inference tests across seven different language models ranging Link to heading

Why Memory Bandwidth Dominates (in Single-Stream Inference) -Link to heading

The roofline numbers tell a clear story: operational intensity ranges from 0.91 to 3.23 FLOPs/byte across all tested models during single-token generation (batch size = 1). To actually saturate those 1024 CUDA cores and hit compute-bound operation, you’d need an operational intensity around 147 FLOPs/byte at the device’s 68 GB/s memory bandwidth.

In practice, for a model to actually become compute-bound on this device during single-stream inference, it would need an operational intensity exceeding:

OI_threshold = Peak_Compute / Memory_Bandwidth
+Link to heading

The roofline numbers tell a clear story: operational intensity ranges from 0.91 to 3.23 FLOPs/byte across all tested models during single-token generation (batch size = 1). To actually saturate those 1024 CUDA cores and hit compute-bound operation, you’d need an operational intensity around 147 FLOPs/byte at the device’s 68 GB/s memory bandwidth.

In practice, for a model to actually become compute-bound on this device during single-stream inference, it would need an operational intensity exceeding:

OI_threshold = Peak_Compute / Memory_Bandwidth
              = (40 × 10^12 ops/s) / (68 × 10^9 bytes/s)
              = 588 FLOPs/byte
 

Single-stream autoregressive decoding falls 100-600× short of this threshold because each token generation requires loading the entire model from memory (matrix-vector multiplication) while performing only ~2 FLOPs per parameter. The compute units are idle most of the time, simply waiting for model weights and activations to arrive from memory.

Note: Production LLM serving with large batch sizes (32-256 requests) changes this dynamic dramatically—batching transforms matrix-vector operations into matrix-matrix multiplications, increasing operational intensity by 30-250× and making workloads compute-bound. However, edge devices serving single users cannot exploit this optimization.

The largest model tested—gemma3n:e2b at 3.5GB quantized (5.44B total parameters, 2B effective)—shows only 16.3% efficiency, similar to other quantized models. Despite being the largest model, Q4_K_M quantization keeps its memory footprint manageable, resulting in similar operational intensity (3.23 FLOPs/byte) to the other INT4-quantized models. Its MatFormer architecture with selective parameter activation (only 2B of 5.44B params active per token) actually helps reduce memory traffic, though this benefit is partially offset by the overhead of routing logic.

What This Means for Edge Deployment @@ -62,4 +62,4 @@ After running 66 inference tests across seven different language models ranging 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/blog-draft/index.html b/posts/blog-draft/index.html new file mode 100644 index 0000000..32cc53c --- /dev/null +++ b/posts/blog-draft/index.html @@ -0,0 +1,50 @@ +Deployment Lessons and My Take on Self-Hosting OpenClaw · Eric X. Liu's Personal Page

Deployment Lessons and My Take on Self-Hosting OpenClaw

Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.

Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.

Layer 1: The Environment – Breaking the Sandbox + +Link to heading

To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.

Code execution agents often need to spin up temporary servers—for previews, React apps, or documentation sites. In a standard Kubernetes Pod, these dynamic ports (like 3000, 8080, etc.) are isolated inside the container network namespace.

To securely expose these arbitrary ports, I deployed a lightweight Nginx sidecar alongside the main OpenClaw agent. This avoids the complexity and latency of dynamically updating Ingress resources.

The Nginx configuration handling the routing logic:

server {
+    listen 80;
+    server_name ~^(?<port>\d+)\.agent\.mydomain\.com$;
+
+    location / {
+        proxy_pass http://localhost:$port;
+        proxy_set_header Host $host;
+    }
+}
+

This configuration uses a regex-based server block to capture the port from the subdomain (e.g., 3000.agent.mydomain.com) and proxies traffic to that port on localhost. Since containers in the same Pod share a network namespace, localhost connectivity is seamless.

For this to work effectively, the agent must be aware of its environment. I updated OpenClaw’s system prompts to understand this pattern: “If you start a server on port X, the external URL is https://X.agent.mydomain.com. This allows the agent to provide valid, clickable links for its generated applications.

Layer 2: The Runtime – Agility and Persistence + +Link to heading

Once the agent allows for external connectivity, the next challenge is agility. Self-hosting often requires customizations that haven’t yet been merged upstream.

Self-hosting often requires customizations that haven’t yet been merged upstream. For example, I needed a custom OAuth flow for Google’s internal APIs.

Instead of maintaining a forked Docker image, I used a Kubernetes ConfigMap to inject the necessary TypeScript plugin at runtime. The file is mounted directly into the container at /app/extensions/google-antigravity-auth/index.ts.

kind: ConfigMap
+metadata:
+  name: openclaw-patch-antigravity
+data:
+  index.ts: |
+    import { createHash, randomBytes } from "node:crypto";
+    // ... custom OAuth implementation ...
+    export default antigravityPlugin;
+

This approach allows for rapid iteration on patches without rebuilding container images for every change.

However, two operational realities became clear during this process:

  1. Debugging is Standard: When the agent fails (e.g., your custom patch throws an error), it behaves like any other application. Standard debugging tools like kubectl logs and strace remain the most effective way to diagnose issues.
  2. Persistent Storage Matches Tooling: Just as code needs injection, tools need persistence. I had to explicitly mount a volume for Homebrew (.linuxbrew) so that tools installed by me or the agent didn’t vanish on pod restart. Agents need long-term memory on their filesystem as much as in their context window.

Layer 3: The Capabilities – Skills over Abstractions + +Link to heading

With the infrastructure (Layer 1) and runtime (Layer 2) established, we move to the application logic: how the agent actually does work.

While the industry chases complex abstractions like the Model Context Protocol (MCP), I found that simple, text-based “Skills” offer a superior workflow. I recently created a Gitea skill simply by exposing the tea CLI documentation to the agent.

This approach aligns with the UNIX philosophy: small, simple tools that do one thing well. MCP servers often clutter the context window and impose significant development overhead. A well-structured “Skill”—essentially a localized knowledge base for a CLI—is cleaner and faster to implement. I predict that these lightweight Skills will eventually replace heavy MCP integrations for the majority of use cases.

There is one current limitation: Gemini models lack specific post-training for these custom skills. The agent doesn’t always intuitively know when to reach for a specific tool. Also, remember that granting the agent access to CLI tools like kubectl or tea (Gitea CLI) enables it to perform operations directly, transforming it from a text generator to a system operator. My agent can now open Pull Requests on my self-hosted Gitea instance, effectively becoming a contributor to its own config repo.

The Payoff: Why This Complexity Matters + +Link to heading

Why go through this trouble of sidecars, config patches, and custom skills?

My previous AI workflows relied on standard chatbots via interfaces like Open-WebUI. The friction in that model is the “all-or-nothing” generation. LLMs are stochastic; regenerating an entire file to change three lines is inefficient and risky.

OpenClaw’s or agentic tools (such as Cursor or Antigravity) killer feature is partial editing. The ability to iteratively improve a stable codebase or document without regenerating the entire file is the missing link for AI-assisted development. We need to treat code as a living document, not a chat response.

When combined with tools like Obsidian that I already use as my second brain for persistent knowledge management, this model provides both the long-term memory and the granular control necessary for complex projects.

References + +Link to heading

  1. OpenClaw Documentation: https://docs.openclaw.org
  2. Kubernetes Flux CD: https://fluxcd.io/
  3. Nginx Regex Server Names: https://nginx.org/en/docs/http/server_names.html
\ No newline at end of file diff --git a/posts/breville-barista-pro-maintenance/index.html b/posts/breville-barista-pro-maintenance/index.html index 4cb8a1c..bdf8f03 100644 --- a/posts/breville-barista-pro-maintenance/index.html +++ b/posts/breville-barista-pro-maintenance/index.html @@ -25,4 +25,4 @@ Understanding the Two Primary Maintenance Cycles Link to heading The Breville Ba 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/debugging-authentik-performance/index.html b/posts/debugging-authentik-performance/index.html index 2f7de4a..b70b566 100644 --- a/posts/debugging-authentik-performance/index.html +++ b/posts/debugging-authentik-performance/index.html @@ -10,7 +10,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS Link to heading

My homelab is designed for node-level resilience, which adds complexity to the storage layer. It is not running on a single server, but rather a 3-node Proxmox cluster where every component is redundant:

  • Orchestration: Kubernetes (k3s) managed via Flux CD.
  • Storage: A Ceph cluster running on the Proxmox nodes, utilizing enterprise NVMe SSDs (bluestore) for OSDs.
  • Database: Postgres managed by the Zalando Postgres Operator, with persistent volumes (PVCs) provisioned on Ceph RBD (block storage).
  • Identity: Authentik for SSO.

While the underlying disks are blazing fast NVMe drives, the architecture dictates that a write to a Ceph RBD volume is not complete until it is replicated over the network and acknowledged by multiple OSDs. This setup provides incredible resilience—I can pull the plug on a node and nothing stops—but it introduces unavoidable network latency for synchronous write operations. Keep this particular trade-off in mind; it plays a starring role in the investigation later.

The Symptom -Link to heading

The issue was insidious because it was intermittent. Clicking “Login” would sometimes hang for 5-8 seconds, while other times it was instant. To an engineer, “sometimes slow” is the worst kind of bug because it defies easy reproduction.

The breakthrough came when I put aside the server-side Grafana dashboards and looked at the client side. By opening Chrome DevTools and monitoring the Network tab during a slow login attempt, I was able to capture the exact failing request.

I identified the culprit: the /api/v3/core/applications/ endpoint. It wasn’t a connection timeout or a DNS issue; the server was simply taking 5+ seconds to respond to this specific GET request.

Armed with this “smoking gun,” I copied the request as cURL (preserving the session cookies) and converted it into a Python benchmark script (reproduce_latency.py). This allowed me to reliably trigger the latency on demand, turning an intermittent “heisenbug” into a reproducible test case.

The results were validating and horrifying:

Request 1: 2.1642s
+Link to heading

The issue was insidious because it was intermittent. Clicking “Login” would sometimes hang for 5-8 seconds, while other times it was instant. To an engineer, “sometimes slow” is the worst kind of bug because it defies easy reproduction.

The breakthrough came when I put aside the server-side Grafana dashboards and looked at the client side. By opening Chrome DevTools and monitoring the Network tab during a slow login attempt, I was able to capture the exact failing request.

I identified the culprit: the /api/v3/core/applications/ endpoint. It wasn’t a connection timeout or a DNS issue; the server was simply taking 5+ seconds to respond to this specific GET request.

Armed with this “smoking gun,” I copied the request as cURL (preserving the session cookies) and converted it into a Python benchmark script (reproduce_latency.py). This allowed me to reliably trigger the latency on demand, turning an intermittent “heisenbug” into a reproducible test case.

The results were validating and horrifying:

Request 1: 2.1642s
 Request 2: 8.4321s
 Request 3: 5.1234s
 ...
@@ -19,7 +19,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
 
 Link to heading

Attempt 1: The Connection Overhead Hypothesis -Link to heading

The Hypothesis: Authentik defaults to CONN_MAX_AGE=0, meaning it closes the database connection after every request. Since I enforce SSL for the database, I assumed the handshake overhead was killing performance.

The Fix Attempt: I updated the Authentik configuration to enable persistent connections:

env:
+Link to heading

The Hypothesis: Authentik defaults to CONN_MAX_AGE=0, meaning it closes the database connection after every request. Since I enforce SSL for the database, I assumed the handshake overhead was killing performance.

The Fix Attempt: I updated the Authentik configuration to enable persistent connections:

env:
   - name: AUTHENTIK_POSTGRESQL__CONN_MAX_AGE
     value: "600"
 

The Reality: The benchmark showed a slight improvement (~4.2s average), but the random 5-8s spikes remained. The 300ms connection setup was a factor, but not the root cause. As a side note, enabling this without configuring TCP Keepalives caused the Authentik worker to crash with OperationalError('the connection is closed') when firewalls silently dropped idle connections.

Attempt 2: CPU Starvation @@ -30,7 +30,7 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS Link to heading

I checked the release notes for Authentik 2025.10:

Breaking Change: Redis is no longer used for caching. All caching has been moved to the PostgreSQL database to simplify deployment.

This architectural shift created a bottleneck specific to my storage backend:

  1. The Change: Every API request triggers a cache write (session updates) to Postgres instead of Redis.
  2. The Default: Postgres defaults to synchronous_commit = on. A transaction is not considered “committed” until it is flushed to disk.
  3. The Storage: Ceph RBD replicates data across the network to multiple OSDs.

Every time I loaded the dashboard, Authentik tried to update the cache. Postgres paused, verified the write was replicated to 3 other servers over the network (WAL Sync), and then responded.

The Solution -Link to heading

I couldn’t move the database to local NVMe without losing the failover capabilities I built the cluster for. However, for a cache-heavy workload, I could compromise on strict durability.

I patched the Postgres configuration to disable synchronous commits:

spec:
+Link to heading

I couldn’t move the database to local NVMe without losing the failover capabilities I built the cluster for. However, for a cache-heavy workload, I could compromise on strict durability.

I patched the Postgres configuration to disable synchronous commits:

spec:
   postgresql:
     parameters:
       synchronous_commit: "off"  # The magic switch
@@ -44,4 +44,4 @@ My detailed Grafana dashboards said everything was fine. But my wife said the SS
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/index.html b/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/index.html
index ea1a576..cbe9706 100644
--- a/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/index.html
+++ b/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/index.html
@@ -20,4 +20,4 @@ Our overarching philosophy is simple: isolate and change only one variable at a
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/flashing-jetson-orin-nano-in-virtualized-environments/index.html b/posts/flashing-jetson-orin-nano-in-virtualized-environments/index.html
index 139583a..14a1e9b 100644
--- a/posts/flashing-jetson-orin-nano-in-virtualized-environments/index.html
+++ b/posts/flashing-jetson-orin-nano-in-virtualized-environments/index.html
@@ -31,7 +31,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 
 Link to heading

Given the constraint of not having an x86 laptop, initial attempts used a QEMU/KVM virtual machine running Ubuntu 22.04 x86_64 on an Apple M4 Mac via UTM (a QEMU frontend for macOS). This approach allowed running SDK Manager on an emulated x86_64 system while connecting the Jetson device via USB passthrough configured through UTM’s USB settings.

While this satisfied the requirement of having an x86_64 environment without using the Proxmox hosts, it introduced additional virtualization overhead as the entire x86_64 instruction set was being emulated on ARM64 hardware.

Issues Encountered -Link to heading

The flash process consistently failed during the USB communication phase with the error:

ERROR: might be timeout in USB write.
+Link to heading

The flash process consistently failed during the USB communication phase with the error:

ERROR: might be timeout in USB write.
 

Root Cause Analysis Link to heading

QEMU/KVM’s USB passthrough implementation has known reliability issues with complex USB protocols. The Jetson’s initrd flash process requires:

  1. Rapid USB re-enumeration when switching between recovery mode and initrd mode
  2. High-throughput data transfer for writing the root filesystem
  3. Bidirectional USB network communication with strict timing requirements

Individual USB device passthrough in QEMU emulates USB at the device level, introducing latency and potential timing issues. The Jetson’s USB networking during initrd boot is particularly sensitive to these delays, causing the timeout errors.

Conclusion @@ -42,7 +42,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host Link to heading

After the Mac-based VM approach failed, attention shifted to the Proxmox infrastructure. LXC containers provide near-native performance with minimal virtualization overhead compared to full VMs. Unlike running SDK Manager directly on the Proxmox host (which was ruled out for stability reasons), an LXC container offers:

  1. Isolation: Complete separation from the host OS with its own filesystem and process space
  2. Near-Native Performance: Containers share the host kernel, eliminating instruction emulation overhead
  3. Easy Management: Containers can be created, destroyed, and backed up without affecting the host
  4. USB Access: Proxmox supports passing USB devices to containers via cgroup device permissions

The hypothesis was that an LXC container with proper USB device access would provide the necessary USB timing characteristics while maintaining the clean separation requirement.

Configuration Progression -Link to heading

The LXC container (ID 106, Ubuntu 22.04) required extensive configuration on the Proxmox host (/etc/pve/lxc/106.conf):

# Enable mknod capability for creating device nodes
+Link to heading

The LXC container (ID 106, Ubuntu 22.04) required extensive configuration on the Proxmox host (/etc/pve/lxc/106.conf):

# Enable mknod capability for creating device nodes
 features: nesting=1,mknod=1
 
 # USB device passthrough (Bus 003)
@@ -59,23 +59,23 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 
 Link to heading

1. mknod Permission Errors -Link to heading

Error: mknod: .../rootfs/dev/random: Operation not permitted

Cause: LXC containers lack CAP_MKNOD capability by default, required by L4T flash scripts to create device nodes in the rootfs.

Solution: Enable mknod=1 feature on the Proxmox host:

pct set 106 -features nesting=1,mknod=1
+Link to heading

Error: mknod: .../rootfs/dev/random: Operation not permitted

Cause: LXC containers lack CAP_MKNOD capability by default, required by L4T flash scripts to create device nodes in the rootfs.

Solution: Enable mknod=1 feature on the Proxmox host:

pct set 106 -features nesting=1,mknod=1
 

2. ARM64 Binary Execution -Link to heading

Error: chroot: failed to run command 'dpkg': Exec format error

Cause: The L4T rootfs contains ARM64 binaries that cannot execute on x86_64 without emulation.

Solution: Install and enable qemu-user-static and binfmt-support on the Proxmox host (not the container):

apt-get install qemu-user-static binfmt-support
+Link to heading

Error: chroot: failed to run command 'dpkg': Exec format error

Cause: The L4T rootfs contains ARM64 binaries that cannot execute on x86_64 without emulation.

Solution: Install and enable qemu-user-static and binfmt-support on the Proxmox host (not the container):

apt-get install qemu-user-static binfmt-support
 update-binfmts --enable qemu-aarch64
 

3. Loop Device Access Link to heading

Error: losetup: cannot find an unused loop device

Cause: The L4T flash scripts use loop devices to mount disk images. LXC containers don’t have loop device access by default.

Solution: Add loop device permissions and mount entries to the container configuration.

4. USB Networking Failure -Link to heading

Error: Device failed to boot to the initrd flash kernel

Cause: This was the most complex issue. When the Jetson boots into initrd mode (0955:7035), it creates a USB network interface (enx* or usb0). However, in LXC containers, this interface appeared in the host’s network namespace, not the container’s namespace.

Attempted Solution:

  1. Loaded USB networking kernel modules on the Proxmox host:
modprobe rndis_host cdc_ether cdc_ncm cdc_subset
+Link to heading

Error: Device failed to boot to the initrd flash kernel

Cause: This was the most complex issue. When the Jetson boots into initrd mode (0955:7035), it creates a USB network interface (enx* or usb0). However, in LXC containers, this interface appeared in the host’s network namespace, not the container’s namespace.

Attempted Solution:

  1. Loaded USB networking kernel modules on the Proxmox host:
modprobe rndis_host cdc_ether cdc_ncm cdc_subset
 echo "rndis_host" >> /etc/modules
 echo "cdc_ether" >> /etc/modules
 echo "cdc_ncm" >> /etc/modules
 echo "cdc_subset" >> /etc/modules
-
  1. Created udev rules to automatically move USB network interfaces to the container:
# /etc/udev/rules.d/99-jetson-usb-network.rules
+
  1. Created udev rules to automatically move USB network interfaces to the container:
# /etc/udev/rules.d/99-jetson-usb-network.rules
 ACTION=="add", SUBSYSTEM=="net", KERNEL=="enx*", RUN+="/usr/local/bin/handle-jetson-usb-network.sh %k"
-
  1. Created handler script to move interfaces into container namespace:
#!/bin/bash
+
  1. Created handler script to move interfaces into container namespace:
#!/bin/bash
 INTERFACE=$1
 CONTAINER_ID=106
 CONTAINER_PID=$(pct exec $CONTAINER_ID -- pidof systemd | awk '{print $1}')
@@ -94,7 +94,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 
 Link to heading

1. Identify USB Controller -Link to heading

# Find which USB controller the Jetson is connected to
+Link to heading
# Find which USB controller the Jetson is connected to
 lsusb -t | grep -B5 "0955:7523"
 
 # Map USB buses to PCI addresses
@@ -102,11 +102,11 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
     pci=$(readlink /sys/bus/usb/devices/usb$bus 2>/dev/null | grep -oE '[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9]')
     echo "USB Bus $bus → PCI $pci"
 done
-

Result: Jetson on Bus 4, controlled by PCI device 0000:22:00.3

Verification that no other critical devices shared this controller:

lsusb | grep "Bus 003"  # Empty except root hub
+

Result: Jetson on Bus 4, controlled by PCI device 0000:22:00.3

Verification that no other critical devices shared this controller:

lsusb | grep "Bus 003"  # Empty except root hub
 lsusb | grep "Bus 004"  # Only Jetson device
 

2. Create VM with PCI Passthrough -Link to heading

# Create VM
+Link to heading
# Create VM
 qm create 200 --name jetson-flash --memory 4096 --cores 4 \
     --net0 virtio,bridge=vmbr0 --scsihw virtio-scsi-pci
 
@@ -134,7 +134,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 qm start 200
 

3. Critical: USB Networking Kernel Modules -Link to heading

The Ubuntu cloud image does not include USB networking kernel modules by default. This is critical because when the Jetson boots into initrd mode, it requires the host to have these modules loaded immediately.

Solution: Install and load modules before starting the flash:

# Install extra kernel modules
+Link to heading

The Ubuntu cloud image does not include USB networking kernel modules by default. This is critical because when the Jetson boots into initrd mode, it requires the host to have these modules loaded immediately.

Solution: Install and load modules before starting the flash:

# Install extra kernel modules
 apt-get install linux-modules-extra-$(uname -r)
 
 # Load USB networking modules
@@ -147,7 +147,7 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 lsmod | grep -E 'rndis|cdc'
 

When the Jetson transitions to initrd mode (0955:7035), the USB network interface (usb0) now appears immediately in the VM’s network namespace.

4. Network Configuration -Link to heading

The Jetson’s initrd uses IPv6 for USB networking by default:

# Interface appears automatically
+Link to heading

The Jetson’s initrd uses IPv6 for USB networking by default:

# Interface appears automatically
 ip addr show usb0
 # Output:
 # usb0: inet6 fc00:1:1::1/64 scope global
@@ -168,4 +168,4 @@ Flashing NVIDIA Jetson devices remotely presents unique challenges when the host
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/how-rvq-teaches-llms-to-see-and-hear/index.html b/posts/how-rvq-teaches-llms-to-see-and-hear/index.html
index 409ed17..183deb6 100644
--- a/posts/how-rvq-teaches-llms-to-see-and-hear/index.html
+++ b/posts/how-rvq-teaches-llms-to-see-and-hear/index.html
@@ -18,4 +18,4 @@ The answer lies in creating a universal language—a bridge between the continuo
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/index.html b/posts/index.html
index 652be76..face94f 100644
--- a/posts/index.html
+++ b/posts/index.html
@@ -1,6 +1,7 @@
 Posts · Eric X. Liu's Personal Page
\ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/index.xml b/posts/index.xml index be66749..a6c59c9 100644 --- a/posts/index.xml +++ b/posts/index.xml @@ -1,4 +1,13 @@ -Posts on Eric X. Liu's Personal Pagehttps://ericxliu.me/posts/Recent content in Posts on Eric X. Liu's Personal PageHugoenThu, 22 Jan 2026 06:48:07 +0000Hacking a Chinese Car Stereo to fulfill my Knight Rider dreamshttps://ericxliu.me/posts/vibe-coding-from-the-jeep/Wed, 21 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/vibe-coding-from-the-jeep/<p>&ldquo;Vibe coding&rdquo; has become my latest obsession. It&rsquo;s that flow state where the tools disappear, and you&rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&rsquo;ve been trying to answer a childhood dream.</p> +Posts on Eric X. Liu's Personal Pagehttps://ericxliu.me/posts/Recent content in Posts on Eric X. Liu's Personal PageHugoenWed, 04 Feb 2026 06:18:45 +0000Deployment Lessons and My Take on Self-Hosting OpenClawhttps://ericxliu.me/posts/blog-draft/Tue, 03 Feb 2026 00:00:00 +0000https://ericxliu.me/posts/blog-draft/<p>Deploying autonomous agents like OpenClaw on a self-hosted Kubernetes cluster offers significantly more control and integration potential than cloud-hosted alternatives. However, moving from a standard SaaS model to running your own intelligence infrastructure introduces several deployment challenges.</p> +<p>Here are the practical lessons learned, organized by the layers of the agentic stack: Environment, Runtime, and Capabilities.</p> +<h2 id="layer-1-the-environment--breaking-the-sandbox"> + Layer 1: The Environment – Breaking the Sandbox + <a class="heading-link" href="#layer-1-the-environment--breaking-the-sandbox"> + <i class="fa-solid fa-link" aria-hidden="true" title="Link to heading"></i> + <span class="sr-only">Link to heading</span> + </a> +</h2> +<p>To move beyond being a chatbot, an agent needs to be able to affect its world. Deep integration starts with networking.</p>Hacking a Chinese Car Stereo to fulfill my Knight Rider dreamshttps://ericxliu.me/posts/vibe-coding-from-the-jeep/Wed, 21 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/vibe-coding-from-the-jeep/<p>&ldquo;Vibe coding&rdquo; has become my latest obsession. It&rsquo;s that flow state where the tools disappear, and you&rsquo;re just manipulating logic at the speed of thought. Usually, this happens in a high-end IDE like Antigravity. But lately, I&rsquo;ve been trying to answer a childhood dream.</p> <p>Growing up in China before the internet age, my window to the outside world was CCTV-6. Along with <em>Baywatch</em>, one of the first American TV shows I ever watched was <em>Knight Rider</em>. I don&rsquo;t remember the exact plot lines, but the core concept stuck with me forever: KITT. A car that could talk, think, and do things for you.</p>How I Built a Blog Agent that Writes About Itselfhttps://ericxliu.me/posts/reverse-engineering-antigravity-ide/Fri, 16 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/reverse-engineering-antigravity-ide/<p>I&rsquo;ve been spending a lot of time &ldquo;vibe coding&rdquo; in the Antigravity IDE lately. It&rsquo;s an incredible flow state—intense, iterative, and fast. But it has a major flaw: the context is ephemeral. Once the session is over, that rich history of decisions, wrong turns, and &ldquo;aha!&rdquo; moments is locked away in an opaque, internal format.</p> <p>I wanted to capture that value. I wanted a system that could take my chaotic coding sessions and distill them into structured, technical blog posts (like the one you&rsquo;re reading right now).</p>Why I Downgraded Magisk to Root My Pixel 2 XLhttps://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/Wed, 07 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/<p>For the past few weeks, I&rsquo;ve been stuck in a stalemate with my EcoFlow Bluetooth Protocol Reverse Engineering Project. I have the hci snoop logs, I have the decompiled APK, and I have a strong suspicion about where the authentication logic is hiding. But suspicion isn&rsquo;t proof.</p> <p>Static analysis has its limits. I found the &ldquo;smoking gun&rdquo; function—a native method responsible for encrypting the login payload—but understanding <em>how</em> it constructs that payload within a strict 13-byte limit purely from assembly (ARM64) was proving to be a headache.</p>Why Your "Resilient" Homelab is Slower Than a Raspberry Pihttps://ericxliu.me/posts/debugging-authentik-performance/Fri, 02 Jan 2026 00:00:00 +0000https://ericxliu.me/posts/debugging-authentik-performance/<p>In the world of self-hosting, there are many metrics for success: 99.9% uptime, sub-second latency, or a perfect GitOps pipeline. But for those of us running &ldquo;production&rdquo; at home, there is only one metric that truly matters: <strong>The Wife Acceptance Factor (WAF)</strong>.</p> diff --git a/posts/jellyfin-sso-with-authentik/index.html b/posts/jellyfin-sso-with-authentik/index.html index 758c9de..ee13304 100644 --- a/posts/jellyfin-sso-with-authentik/index.html +++ b/posts/jellyfin-sso-with-authentik/index.html @@ -17,7 +17,7 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe Link to heading

The configuration is best handled via API (curl) rather than the UI, as it ensures all fields are correctly typed and persistent.

1. Authentik (Terraform) -Link to heading

Let Authentik manage the secrets. Don’t hardcode them.

resource "authentik_provider_oauth2" "jellyfin" {
+Link to heading

Let Authentik manage the secrets. Don’t hardcode them.

resource "authentik_provider_oauth2" "jellyfin" {
   name          = "Jellyfin"
   client_id     = "jellyfin-ericxliu-me"
   # client_secret omitted -> auto-generated
@@ -31,7 +31,7 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe
 }
 

2. Jellyfin Plugin (Bash/Curl) -Link to heading

# ... (retrieve secret from terraform) ...
+Link to heading
# ... (retrieve secret from terraform) ...
 curl -X POST "https://jellyfin.ericxliu.me/SSO/OID/Add/authentik" ... -d '{
    "OidClientId": "jellyfin-ericxliu-me",
    "OidSecret": "'"${SECRET}"'",
@@ -44,13 +44,13 @@ The Setup Link to heading The configuration is best handled via API (curl) rathe
 Link to heading

Because the plugin is still maturing, it doesn’t always handle configuration errors gracefully. Here are the two main “cryptic” failures I encountered.

1. The “Value cannot be null” Crash Link to heading

The Symptom: -You attempt to start the SSO flow and get a generic 500 error. The Jellyfin logs show a C# exception:

System.ArgumentNullException: Value cannot be null. (Parameter 'source')
+You attempt to start the SSO flow and get a generic 500 error. The Jellyfin logs show a C# exception:

System.ArgumentNullException: Value cannot be null. (Parameter 'source')
    at System.Linq.Enumerable.Prepend[TSource](IEnumerable`1 source, TSource element)
    at Jellyfin.Plugin.SSO.Api.SSOController.OidChallenge(...)
 

The Reality: This looks like deep internal failure, but it’s actually a simple configuration miss. The plugin code attempts to prepend “openid profile” to your configured scopes without checking if your scopes array exists first. The Fix: -You must explicitly provide "OidScopes" in your JSON configuration. It cannot be null or omitted.

"OidScopes": ["openid", "profile", "email", "groups"]
+You must explicitly provide "OidScopes" in your JSON configuration. It cannot be null or omitted.

"OidScopes": ["openid", "profile", "email", "groups"]
 

2. The HTTP/HTTPS Mismatch (Redirect Loop) Link to heading

The Symptom: @@ -58,7 +58,7 @@ Authentik rejects the authorization request with “Redirect URI mismatch&rd The Reality: Jellyfin often sits behind a reverse proxy (Ingress/Traefik) terminating TLS. Use Browser Developer Tools to inspect the network requests. You will likely see the redirect_uri parameter encoded as http://jellyfin... instead of https://. configuration. The Fix: -Do not rely on header forwarding magic. Force the scheme in the plugin configuration:

"SchemeOverride": "https"
+Do not rely on header forwarding magic. Force the scheme in the plugin configuration:

"SchemeOverride": "https"
 

3. Case Sensitivity in JSON Link to heading

The Symptom: Configuration seems to be ignored or fields remain empty after a POST. @@ -71,4 +71,4 @@ Do not rely on header forwarding magic. Force the scheme in the plugin configura 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/index.html b/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/index.html index 701acf3..b6a89d9 100644 --- a/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/index.html +++ b/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/index.html @@ -44,4 +44,4 @@ The Top-K routing mechanism, as illustrated in the provided ima 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/open-webui-openai-websearch/index.html b/posts/open-webui-openai-websearch/index.html index 7dab207..e6cd889 100644 --- a/posts/open-webui-openai-websearch/index.html +++ b/posts/open-webui-openai-websearch/index.html @@ -10,7 +10,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest Link to heading

  1. Wrong API surface. /v1/chat/completions still rejects type: "web_search" with Invalid value: 'web_search'. Supported values are: 'function' and 'custom'.
  2. LiteLLM tooling gap. The OpenAI TypedDicts in litellm/types/llms/openai.py only allow Literal["function"]. Even if the backend call succeeded, streaming would crash when it saw a new tool type.
  3. Open WebUI assumptions. The UI eagerly parses every tool delta, so when LiteLLM streamed the raw web_search_call chunk, the UI tried to execute it, failed to parse the arguments, and aborted the chat.

Fixing all three required touching both the proxy configuration and the LiteLLM transformation path.

Step 1 – Route GPT‑5 Through the Responses API -Link to heading

LiteLLM’s Responses bridge activates whenever the backend model name starts with openai/responses/. I added a dedicated alias, gpt-5.2-search, that hardcodes the Responses API plus web search metadata. Existing models (reasoning, embeddings, TTS) stay untouched.

# proxy-config.yaml (sanitized)
+Link to heading

LiteLLM’s Responses bridge activates whenever the backend model name starts with openai/responses/. I added a dedicated alias, gpt-5.2-search, that hardcodes the Responses API plus web search metadata. Existing models (reasoning, embeddings, TTS) stay untouched.

# proxy-config.yaml (sanitized)
 model_list:
   - model_name: gpt-5.2-search
     litellm_params:
@@ -25,7 +25,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
             country: US
 

Any client (Open WebUI included) can now request model: "gpt-5.2-search" over the standard /v1/chat/completions endpoint, and LiteLLM handles the Responses API hop transparently.

Step 2 – Mask web_search_call Chunks Inside LiteLLM -Link to heading

Even with the right API, LiteLLM still needs to stream deltas Open WebUI can digest. My hotfix.py script copies the LiteLLM source into /tmp/patch/litellm, then rewrites two files. This script runs as part of the Helm release’s init hook so I can inject fixes directly into the container filesystem at pod start. That saves me from rebuilding and pushing new images every time LiteLLM upstream changes (or refuses a patch), which is critical while waiting for issue #13042 to land. I’ll try to upstream the fix, but this is admittedly hacky, so timelines are uncertain.

  1. openai.py TypedDicts: extend the tool chunk definitions to accept Literal["web_search"].
  2. litellm_responses_transformation/transformation.py: intercept every streaming item and short-circuit anything with type == "web_search_call", returning an empty assistant delta instead of a tool call.
# Excerpt from hotfix.py
+Link to heading

Even with the right API, LiteLLM still needs to stream deltas Open WebUI can digest. My hotfix.py script copies the LiteLLM source into /tmp/patch/litellm, then rewrites two files. This script runs as part of the Helm release’s init hook so I can inject fixes directly into the container filesystem at pod start. That saves me from rebuilding and pushing new images every time LiteLLM upstream changes (or refuses a patch), which is critical while waiting for issue #13042 to land. I’ll try to upstream the fix, but this is admittedly hacky, so timelines are uncertain.

  1. openai.py TypedDicts: extend the tool chunk definitions to accept Literal["web_search"].
  2. litellm_responses_transformation/transformation.py: intercept every streaming item and short-circuit anything with type == "web_search_call", returning an empty assistant delta instead of a tool call.
# Excerpt from hotfix.py
 tool_call_chunk_original = (
     'class ChatCompletionToolCallChunk(TypedDict):  # result of /chat/completions call\n'
     '    id: Optional[str]\n'
@@ -37,7 +37,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
 ...
 if tool_call_chunk_original in content:
     content = content.replace(tool_call_chunk_original, tool_call_chunk_patch, 1)
-
added_block = """            elif output_item.get("type") == "web_search_call":
+
added_block = """            elif output_item.get("type") == "web_search_call":
                 # Mask the call: Open WebUI should never see tool metadata
                 action_payload = output_item.get("action")
                 verbose_logger.debug(
@@ -57,7 +57,7 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
 """
 

These patches ensure LiteLLM never emits a tool_calls delta for web_search. Open WebUI only receives assistant text chunks, so it happily renders the model response and the inline citations the Responses API already provides.

Step 3 – Prove It with cURL (and Open WebUI) -Link to heading

I keep a simple smoke test (litellm_smoke_test.sh) that hits the public ingress with and without streaming. The only secrets are placeholders here, but the structure is the same.

#!/usr/bin/env bash
+Link to heading

I keep a simple smoke test (litellm_smoke_test.sh) that hits the public ingress with and without streaming. The only secrets are placeholders here, but the structure is the same.

#!/usr/bin/env bash
 set -euo pipefail
 
 echo "Testing non-streaming..."
@@ -86,4 +86,4 @@ This post documents the final setup, the hotfix script that keeps LiteLLM honest
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/openwrt-mwan3-wireguard-endpoint-exclusion/index.html b/posts/openwrt-mwan3-wireguard-endpoint-exclusion/index.html
index e36d9e1..198b5eb 100644
--- a/posts/openwrt-mwan3-wireguard-endpoint-exclusion/index.html
+++ b/posts/openwrt-mwan3-wireguard-endpoint-exclusion/index.html
@@ -16,7 +16,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
 
 Link to heading
  • OpenWrt 24.10.x
  • MWAN3 for multi-WAN policy routing
  • WireGuard interface configured with broad allowed_ips covering default route (0.0.0.0/1 and 128.0.0.0/1 or 0.0.0.0/0)

Symptoms -Link to heading

  • wg show indicates the interface is listening, but transfer: 0 B received persists after bringing the tunnel up.
  • Intermittent reachability to public IPs until routing settles.
  • ip route shows multiple defaults via WANs; a host route to the peer IP may exist but is still overridden by policy routing once MWAN3 applies rules.

Example observations (sanitized):

wg show
+Link to heading
  • wg show indicates the interface is listening, but transfer: 0 B received persists after bringing the tunnel up.
  • Intermittent reachability to public IPs until routing settles.
  • ip route shows multiple defaults via WANs; a host route to the peer IP may exist but is still overridden by policy routing once MWAN3 applies rules.

Example observations (sanitized):

wg show
 interface: wireguard
   public key: <redacted>
   listening port: 39345
@@ -33,22 +33,22 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
 
 Link to heading

With default-route allowed_ips, WireGuard installs routes so that all outbound traffic prefers the tunnel. MWAN3 then applies policy rules that also match “all traffic,” including the UDP packets to the WireGuard peer’s public IP. If those packets are selected to go via the wireguard interface (or a table whose default is the tunnel), the handshake cannot succeed. This creates a chicken-and-egg dependency.

Fix: Exclude the WireGuard Endpoint from MWAN3 Default Policy -Link to heading

Force traffic to the WireGuard peer public endpoint to use a physical WAN policy. This guarantees the handshake packets always reach the Internet outside of the tunnel.

Steps:

  1. Resolve the peer endpoint IP (if you only have a hostname)
nslookup vpn.example.com
+Link to heading

Force traffic to the WireGuard peer public endpoint to use a physical WAN policy. This guarantees the handshake packets always reach the Internet outside of the tunnel.

Steps:

  1. Resolve the peer endpoint IP (if you only have a hostname)
nslookup vpn.example.com
 # => use the returned A/AAAA address(es) in the rule below
-
  1. Add an MWAN3 rule targeting the endpoint IP

Edit /etc/config/mwan3 and place this rule before the default v4 rule so it takes precedence:

config rule 'wireguard_endpoint'
+
  1. Add an MWAN3 rule targeting the endpoint IP

Edit /etc/config/mwan3 and place this rule before the default v4 rule so it takes precedence:

config rule 'wireguard_endpoint'
     option dest_ip '203.0.113.55'   # peer public IP
     option proto 'udp'
     option use_policy 'wan_only'     # a policy that prefers a physical WAN
     option family 'ipv4'
-

Notes:

  • Use the actual public IP of your WireGuard server. MWAN3 rules match IPs, not hostnames.
  • If you have multiple WAN policies (e.g., wan_only, wphone_only), choose the one that must carry the VPN handshake.
  1. (Optional) Assign a metric on the WireGuard interface

This is not strictly required for the fix but keeps routing behavior deterministic when multiple defaults exist.

Edit /etc/config/network:

config interface 'wireguard'
+

Notes:

  • Use the actual public IP of your WireGuard server. MWAN3 rules match IPs, not hostnames.
  • If you have multiple WAN policies (e.g., wan_only, wphone_only), choose the one that must carry the VPN handshake.
  1. (Optional) Assign a metric on the WireGuard interface

This is not strictly required for the fix but keeps routing behavior deterministic when multiple defaults exist.

Edit /etc/config/network:

config interface 'wireguard'
     option proto 'wireguard'
     option private_key '<redacted>'
     list addresses '192.168.3.2/32'
     option metric '5'
-
  1. Apply changes
/etc/init.d/network restart && /etc/init.d/mwan3 restart
+
  1. Apply changes
/etc/init.d/network restart && /etc/init.d/mwan3 restart
 

Validation -Link to heading

Confirm that the endpoint is routed via a physical WAN and that the tunnel is passing traffic.

# Verify policy routing for the endpoint
+Link to heading

Confirm that the endpoint is routed via a physical WAN and that the tunnel is passing traffic.

# Verify policy routing for the endpoint
 ip route get 203.0.113.55
 
 # MWAN3 status should show your WANs online
@@ -60,7 +60,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
 
 Link to heading
  • Endpoint IP changes: If the server endpoint is behind DDNS, you must update the rule when its IP changes. Options include:
    • Use a small script triggered by DDNS updates to modify the MWAN3 rule and reload.
    • Maintain an IP set and populate it from DDNS; match the set in firewall/PBR and keep MWAN3 in sync.
  • IPv6: Repeat the approach with an IPv6 rule if your peer uses IPv6. Ensure family 'ipv6' and the correct policy are set.
  • Multiple peers: Create one rule per peer endpoint IP.
  • Ordering: Keep the endpoint rule above broad default rules so it always wins.

Minimal Example Config Snippets (Sanitized) -Link to heading

/etc/config/network (relevant parts):

config interface 'wireguard'
+Link to heading

/etc/config/network (relevant parts):

config interface 'wireguard'
     option proto 'wireguard'
     option private_key '<redacted>'
     list addresses '192.168.3.2/32'
@@ -74,7 +74,7 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
     list allowed_ips '0.0.0.0/1'
     list allowed_ips '128.0.0.0/1'
     option route_allowed_ips '1'
-

/etc/config/mwan3 (relevant parts):

config policy 'wan_only'
+

/etc/config/mwan3 (relevant parts):

config policy 'wan_only'
     list use_member 'wan_m1_w3'
     option last_resort 'unreachable'
 
@@ -98,4 +98,4 @@ When using WireGuard together with MWAN3 on OpenWrt, the tunnel can fail to esta
 2016 -
 2026
 Eric X. Liu
-[6100dca]
\ No newline at end of file
+[45629c5]
\ No newline at end of file
diff --git a/posts/page/2/index.html b/posts/page/2/index.html
index 3259c7a..6cd1e1f 100644
--- a/posts/page/2/index.html
+++ b/posts/page/2/index.html
@@ -1,6 +1,7 @@
 Posts · Eric X. Liu's Personal Page
\ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/page/3/index.html b/posts/page/3/index.html index 5a3a50e..2ab1f09 100644 --- a/posts/page/3/index.html +++ b/posts/page/3/index.html @@ -1,6 +1,7 @@ Posts · Eric X. Liu's Personal Page
\ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/ppo-for-language-models/index.html b/posts/ppo-for-language-models/index.html index 90fbb74..6a2754a 100644 --- a/posts/ppo-for-language-models/index.html +++ b/posts/ppo-for-language-models/index.html @@ -25,4 +25,4 @@ where δ_t = r_t + γV(s_{t+1}) - V(s_t)

  • γ (gam 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/quantization-in-llms/index.html b/posts/quantization-in-llms/index.html index 9d66fa0..df3f07b 100644 --- a/posts/quantization-in-llms/index.html +++ b/posts/quantization-in-llms/index.html @@ -7,4 +7,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/reverse-engineering-antigravity-ide/index.html b/posts/reverse-engineering-antigravity-ide/index.html index 7069d66..c2540dc 100644 --- a/posts/reverse-engineering-antigravity-ide/index.html +++ b/posts/reverse-engineering-antigravity-ide/index.html @@ -24,4 +24,4 @@ I wanted to capture that value. I wanted a system that could take my chaotic cod 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/rooting-pixel-2-xl-for-reverse-engineering/index.html b/posts/rooting-pixel-2-xl-for-reverse-engineering/index.html index 4ad4db8..fbfd2be 100644 --- a/posts/rooting-pixel-2-xl-for-reverse-engineering/index.html +++ b/posts/rooting-pixel-2-xl-for-reverse-engineering/index.html @@ -15,17 +15,17 @@ Static analysis has its limits. I found the “smoking gun” function—a nativ Link to heading

    Once in, I checked the version: Android 10 (QP1A.190711.020). This was ancient. The Pixel 2 XL officially supports Android 11, and I wanted the latest possible base for compatibility with modern tools.

    I tried the easy route: Settings > System Update. The Result: Failure. The phone refused to pull the final OTA (RP1A.201005.004.A1), likely due to the Google update servers no longer prioritizing this EOL device.

    The Fix: Manual Flashing -Link to heading

    I had to bypass the OTA system entirely. I downloaded the final Factory Image from Google.

    # Don't rely on OTA. Flash the whole valid state.
    +Link to heading

    I had to bypass the OTA system entirely. I downloaded the final Factory Image from Google.

    # Don't rely on OTA. Flash the whole valid state.
     fastboot -w update image-taimen-rp1a.201005.004.a1.zip
     

    Note: I used the -w flag here since I had just wiped the device anyway. This gave me a pristine, stock Android 11 environment to break.

    Phase 3: The Magisk “Time Travel” Link to heading

    This is where “modern tools meets old hardware” caused the most pain.

    The Hypothesis: Rooting a Pixel is standard procedure.

    1. Extract boot.img from the factory zip.
    2. Patch it with the latest Magisk app.
    3. Flash it back.

    The Reality: Bootloop. I used Magisk v30.6 (the latest as of writing). The patch process “succeeded,” but flashing the resulting image caused the phone to immediately crash back to the bootloader with a “Cannot find valid operating system” error.

    Debugging the Bootloop -Link to heading

    I suspected a regression in how modern Magisk handles the antiquated boot partition structure of the Pixel 2 (A/B partitions, but pre-GKI).

    I decided to perform some “software archaeology” and use a version of Magisk that was contemporary with the device’s lifespan. I grabbed Magisk v25.0 (released around 2022).

    1. Repatch: I patched the exact same stock boot.img using the v25.0 app.
    2. Reflash:
    # Flash to both slots to be safe
    +Link to heading

    I suspected a regression in how modern Magisk handles the antiquated boot partition structure of the Pixel 2 (A/B partitions, but pre-GKI).

    I decided to perform some “software archaeology” and use a version of Magisk that was contemporary with the device’s lifespan. I grabbed Magisk v25.0 (released around 2022).

    1. Repatch: I patched the exact same stock boot.img using the v25.0 app.
    2. Reflash:
    # Flash to both slots to be safe
     fastboot flash boot_a magisk_patched_25000.img
     fastboot flash boot_b magisk_patched_25000.img
    -

    The Result: Success. The phone booted, and the Magisk app confirmed Installed: 25.0.

    ❯ adb shell "su -c id"
    +

    The Result: Success. The phone booted, and the Magisk app confirmed Installed: 25.0.

    ❯ adb shell "su -c id"
     uid=0(root) gid=0(root) groups=0(root) context=u:r:magisk:s0
     

    Key Insights @@ -35,4 +35,4 @@ I used Magisk v30.6 (the latest as of writing). The patch proce 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/secure-boot-dkms-and-mok-on-proxmox-debian/index.html b/posts/secure-boot-dkms-and-mok-on-proxmox-debian/index.html index 80be6cc..d678a7b 100644 --- a/posts/secure-boot-dkms-and-mok-on-proxmox-debian/index.html +++ b/posts/secure-boot-dkms-and-mok-on-proxmox-debian/index.html @@ -18,36 +18,36 @@ nvidia-smi failed to communicate with the NVIDIA driver modprobe nvidia → “K Link to heading

    Keep Secure Boot on; get modules trusted. That requires:

    1. Ensure the VM boots via shim (so MOK can work)
    2. Make sure DKMS signs modules with a MOK key/cert
    3. Enroll that MOK into the firmware via shim’s MokManager

    Step 1 — Boot via shim and persist EFI variables -Link to heading

    In Proxmox (VM stopped):

    • BIOS: OVMF (UEFI)
    • Add EFI Disk (stores OVMF VARS; required for MOK)
    • Machine: q35
    • Enable Secure Boot (option shows only with OVMF + EFI Disk)

    Inside Debian:

    • Ensure ESP is mounted at /boot/efi
    • Install signed boot stack:
      sudo apt install shim-signed grub-efi-amd64-signed efibootmgr mokutil
      +Link to heading

      In Proxmox (VM stopped):

      • BIOS: OVMF (UEFI)
      • Add EFI Disk (stores OVMF VARS; required for MOK)
      • Machine: q35
      • Enable Secure Boot (option shows only with OVMF + EFI Disk)

      Inside Debian:

      • Ensure ESP is mounted at /boot/efi
      • Install signed boot stack:
        sudo apt install shim-signed grub-efi-amd64-signed efibootmgr mokutil
         sudo grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian
         sudo update-grub
        -
      • Create/verify a boot entry that points to shim:
        sudo efibootmgr -c -d /dev/sda -p 15 -L "debian" -l '\EFI\debian\shimx64.efi'
        +
      • Create/verify a boot entry that points to shim:
        sudo efibootmgr -c -d /dev/sda -p 15 -L "debian" -l '\EFI\debian\shimx64.efi'
         sudo efibootmgr -o 0002,0001,0000     # make shim (0002) first
         sudo efibootmgr -n 0002               # BootNext shim for the next reboot
        -

      Tip: If NVRAM resets or fallback path is used, copy as a fallback:

      sudo mkdir -p /boot/efi/EFI/BOOT
      +

    Tip: If NVRAM resets or fallback path is used, copy as a fallback:

    sudo mkdir -p /boot/efi/EFI/BOOT
     sudo cp /boot/efi/EFI/debian/shimx64.efi /boot/efi/EFI/BOOT/BOOTX64.EFI
     sudo cp /boot/efi/EFI/debian/{mmx64.efi,grubx64.efi} /boot/efi/EFI/BOOT/
     

    Step 2 — Make DKMS sign NVIDIA modules with a MOK -Link to heading

    Debian already generated a DKMS key at /var/lib/dkms/mok.key. Create an X.509 cert in DER format:

    sudo openssl req -new -x509 \
    +Link to heading

    Debian already generated a DKMS key at /var/lib/dkms/mok.key. Create an X.509 cert in DER format:

    sudo openssl req -new -x509 \
       -key /var/lib/dkms/mok.key \
       -out /var/lib/dkms/mok.der \
       -outform DER \
       -subj "/CN=DKMS MOK/" \
       -days 36500
    -

    Enable DKMS signing:

    sudo sed -i 's|^mok_signing_key=.*|mok_signing_key=/var/lib/dkms/mok.key|' /etc/dkms/framework.conf
    +

    Enable DKMS signing:

    sudo sed -i 's|^mok_signing_key=.*|mok_signing_key=/var/lib/dkms/mok.key|' /etc/dkms/framework.conf
     sudo sed -i 's|^mok_certificate=.*|mok_certificate=/var/lib/dkms/mok.der|' /etc/dkms/framework.conf
    -

    Rebuild/install modules (signs them now):

    sudo dkms build nvidia/$(modinfo -F version nvidia) -k $(uname -r) --force
    +

    Rebuild/install modules (signs them now):

    sudo dkms build nvidia/$(modinfo -F version nvidia) -k $(uname -r) --force
     sudo dkms install nvidia/$(modinfo -F version nvidia) -k $(uname -r) --force
     

    Step 3 — Enroll the MOK via shim (MokManager) -Link to heading

    Queue the cert and set a longer prompt timeout:

    sudo mokutil --revoke-import
    +Link to heading

    Queue the cert and set a longer prompt timeout:

    sudo mokutil --revoke-import
     sudo mokutil --import /var/lib/dkms/mok.der
     sudo mokutil --timeout 30
     sudo efibootmgr -n 0002  # ensure next boot goes through shim
     

    Reboot to the VM console (not SSH). In the blue MOK UI:

    • Enroll MOK → Continue → Yes → enter password → reboot

    If arrow keys don’t work in Proxmox noVNC:

    • Use SPICE (virt-viewer), or
    • From the Proxmox host, send keys:
      • qm sendkey <VMID> down, qm sendkey <VMID> ret, qm sendkey <VMID> esc

    Verification -Link to heading

    sudo mokutil --test-key /var/lib/dkms/mok.der   # “already enrolled”
    +Link to heading
    sudo mokutil --test-key /var/lib/dkms/mok.der   # “already enrolled”
     sudo modprobe nvidia
     nvidia-smi
     kubectl -n gpu-operator get pods -o wide
    @@ -59,4 +59,4 @@ nvidia-smi failed to communicate with the NVIDIA driver modprobe nvidia → “K
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/supabase-deep-dive/index.html b/posts/supabase-deep-dive/index.html
    index df303a5..d91d74f 100644
    --- a/posts/supabase-deep-dive/index.html
    +++ b/posts/supabase-deep-dive/index.html
    @@ -16,8 +16,8 @@ Supabase enters this space with a radically different philosophy: transparency.
     
     Link to heading

    Translate your model into SQL. For any serious project, use the Supabase CLI to manage this process.

    1. Develop Locally: Run a full Supabase stack on your machine with supabase start.
    2. Create Migration Files: Write your CREATE TABLE statements in SQL files. Define columns, data types, and foreign key REFERENCES to enforce your relationships.
    3. Version Control: Commit these migration files to Git. Your database schema is now version-controlled alongside your application code.
    4. Deploy: Use supabase db push to apply your migrations to your live production database. This workflow is safe, repeatable, and professional.

    Phase 3: The Security Layer (Row Level Security) -Link to heading

    This is not an optional step. RLS is the cornerstone of Supabase security.

    1. Deny by Default: For any table holding user data, immediately enable RLS. This blocks all access until you explicitly grant it.
    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
    -
    1. Write “Allow” Policies: Create policies based on your user stories. Policies are SQL rules that the database enforces on every single query.
    -- Users can see tasks in projects they are a member of.
    +Link to heading

    This is not an optional step. RLS is the cornerstone of Supabase security.

    1. Deny by Default: For any table holding user data, immediately enable RLS. This blocks all access until you explicitly grant it.
    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
    +
    1. Write “Allow” Policies: Create policies based on your user stories. Policies are SQL rules that the database enforces on every single query.
    -- Users can see tasks in projects they are a member of.
     CREATE POLICY "Allow read access to tasks in user's projects"
     ON tasks FOR SELECT
     USING (
    @@ -34,8 +34,8 @@ Supabase enters this space with a radically different philosophy: transparency.
     WITH CHECK ( auth.uid() = tasks.assignee_id );
     

    The auth.uid() function is a special Supabase utility that securely returns the ID of the logged-in user making the request.

    Phase 4: The APIs (Data Access) -Link to heading

    With your data structured and secured, you can now build the access points.

    • For Simple CRUD: Use Supabase’s auto-generated API. It’s convenient, respects all your RLS policies, and is perfect for simple reads and writes on a single table.
    const { data, error } = await supabase.from('tasks').select('*');
    -
    • For Complex Logic: Use PostgreSQL Functions (RPC). Encapsulate complex JOINs or multi-step transactions into a single, callable function. This reduces network chattiness and keeps your business logic secure on the server.
    -- A function to get a task and its project name in one call
    +Link to heading

    With your data structured and secured, you can now build the access points.

    • For Simple CRUD: Use Supabase’s auto-generated API. It’s convenient, respects all your RLS policies, and is perfect for simple reads and writes on a single table.
    const { data, error } = await supabase.from('tasks').select('*');
    +
    • For Complex Logic: Use PostgreSQL Functions (RPC). Encapsulate complex JOINs or multi-step transactions into a single, callable function. This reduces network chattiness and keeps your business logic secure on the server.
    -- A function to get a task and its project name in one call
     CREATE OR REPLACE FUNCTION get_task_with_project(task_id_input int)
     RETURNS TABLE (task_title text, project_name text) AS $$
     BEGIN
    @@ -46,18 +46,18 @@ Supabase enters this space with a radically different philosophy: transparency.
         WHERE tasks.id = task_id_input;
     END;
     $$ LANGUAGE plpgsql;
    -
    // Called simply from the frontend
    +
    // Called simply from the frontend
     const { data, error } = await supabase.rpc('get_task_with_project', { task_id_input: 123 });
     

    A Tour of the Core Services Link to heading

    Beyond the database, Supabase provides a suite of essential tools.

    Authentication -Link to heading

    A complete user management system that integrates directly with your database. When a user signs up, a corresponding entry is created in the managed auth.users table, which you can then reference in your own tables.

    // Sign up a new user and handle social logins with ease
    +Link to heading

    A complete user management system that integrates directly with your database. When a user signs up, a corresponding entry is created in the managed auth.users table, which you can then reference in your own tables.

    // Sign up a new user and handle social logins with ease
     const { data, error } = await supabase.auth.signUp({ email, password });
     const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github' });
     

    Storage -Link to heading

    A simple, S3-compatible object store for managing files like user avatars or documents. It’s integrated with Postgres and RLS, allowing you to write fine-grained access policies on files and folders (buckets).

    // Upload a user avatar to a public 'avatars' bucket
    +Link to heading

    A simple, S3-compatible object store for managing files like user avatars or documents. It’s integrated with Postgres and RLS, allowing you to write fine-grained access policies on files and folders (buckets).

    // Upload a user avatar to a public 'avatars' bucket
     const { error } = await supabase.storage
       .from('avatars')
       .upload(`public/${userId}.png`, file);
    @@ -71,7 +71,7 @@ Supabase enters this space with a radically different philosophy: transparency.
     
     Link to heading

    The most important guarantee of this system is its relationship with database transactions. An event is only broadcast after a transaction is fully and successfully committed. If a transaction is rolled back due to an error, the replication slot receives nothing, and no Realtime event is ever sent. This means you can trust that every Realtime message you receive corresponds to data that is permanently and consistently stored in your database.

    Use Cases and Limitations -Link to heading

    • Use For: Small, JSON-based messages like chat messages, live notifications, activity feeds, and presence indicators (“who’s online”). Use the broadcast feature for ephemeral data like cursor positions that you don’t need to save.
    • Do NOT Use For: Large, continuous data streams. It is not a replacement for WebRTC for video/audio calls. The system is designed for small, infrequent payloads.
    const channel = supabase.channel('public:messages');
    +Link to heading
    • Use For: Small, JSON-based messages like chat messages, live notifications, activity feeds, and presence indicators (“who’s online”). Use the broadcast feature for ephemeral data like cursor positions that you don’t need to save.
    • Do NOT Use For: Large, continuous data streams. It is not a replacement for WebRTC for video/audio calls. The system is designed for small, infrequent payloads.
    const channel = supabase.channel('public:messages');
     
     // Subscribe to new rows in the 'messages' table
     channel
    @@ -90,4 +90,4 @@ Supabase enters this space with a radically different philosophy: transparency.
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/index.html b/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/index.html
    index 52277c4..07b33ba 100644
    --- a/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/index.html
    +++ b/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/index.html
    @@ -30,4 +30,4 @@ But to truly understand the field, we must look at the pivotal models that explo
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/technical-deep-dive-llm-categorization/index.html b/posts/technical-deep-dive-llm-categorization/index.html
    index c5643d0..40798d2 100644
    --- a/posts/technical-deep-dive-llm-categorization/index.html
    +++ b/posts/technical-deep-dive-llm-categorization/index.html
    @@ -10,7 +10,7 @@ For years, I relied on a rule-based system to categorize our credit card transac
     
     Link to heading

    My first step was to replace the spaghetti code of regex rules with a prompt. I used Gemini-3-Flash (via litellm) as my categorization engine.

    The core challenge was context. A transaction like MCDONALDS could be:

    • Dining: A quick lunch during work.
    • Travel-Dining: A meal while on a road trip.

    To solve this, I integrated my private Google Calendar (via .ics export). The prompt doesn’t just see the transaction; it sees where I was and what I was doing on that day.

    The “God Prompt” -Link to heading

    The system prompt was designed to return strict JSON, adhering to a schema of Categories (e.g., Dining, Travel, Bills) and Sub-Categories (e.g., Travel -> Accommodation).

    {
    +Link to heading

    The system prompt was designed to return strict JSON, adhering to a schema of Categories (e.g., Dining, Travel, Bills) and Sub-Categories (e.g., Travel -> Accommodation).

    {
       "Category": "Travel",
       "Travel Category": "Dining",
       "Reasoning": "User is on 'Trip: 34TH ARCH CANYON 2025', distinguishing this from regular dining."
    @@ -19,7 +19,7 @@ For years, I relied on a rule-based system to categorize our credit card transac
     
     Link to heading

    I wanted to train a smaller model to mimic Gemini’s performance. But I didn’t want to manually label thousands of transactions.

    Consistency Filtering -Link to heading

    I had a massive CSV of historical transactions (years of data). However, that data was “noisy”—some manual labels were outdated or inconsistent.

    I built a Distillation Pipeline (distill_reasoning.py) that uses the Teacher Model (Gemini) to re-label the historical data. But here’s the twist: I only added a data point to my training set if the Teacher’s prediction matched the Historical Ground Truth.

    # Pseudo-code for consistency filtering
    +Link to heading

    I had a massive CSV of historical transactions (years of data). However, that data was “noisy”—some manual labels were outdated or inconsistent.

    I built a Distillation Pipeline (distill_reasoning.py) that uses the Teacher Model (Gemini) to re-label the historical data. But here’s the twist: I only added a data point to my training set if the Teacher’s prediction matched the Historical Ground Truth.

    # Pseudo-code for consistency filtering
     teacher_pred = gemini.categorize(transaction)
     historical_label = row['Category']
     
    @@ -73,4 +73,4 @@ It turned out to be a syntax error in my arguments passed to the Trainer[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/index.html b/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/index.html
    index 417bae2..12a2890 100644
    --- a/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/index.html
    +++ b/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/index.html
    @@ -26,4 +26,4 @@ This article explores the mathematical equivalence between Hinton’s concept of
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/transformer-s-core-mechanics/index.html b/posts/transformer-s-core-mechanics/index.html
    index 1aa51a6..f98d0fb 100644
    --- a/posts/transformer-s-core-mechanics/index.html
    +++ b/posts/transformer-s-core-mechanics/index.html
    @@ -36,4 +36,4 @@ In deep learning, a “channel” can be thought of as a feature dimensi
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/unifi-vlan-migration-to-zone-based-architecture/index.html b/posts/unifi-vlan-migration-to-zone-based-architecture/index.html
    index 5914477..4ebf3fe 100644
    --- a/posts/unifi-vlan-migration-to-zone-based-architecture/index.html
    +++ b/posts/unifi-vlan-migration-to-zone-based-architecture/index.html
    @@ -28,4 +28,4 @@ This article documents that journey. It details the pitfalls encountered, the co
     2016 -
     2026
     Eric X. Liu
    -[6100dca]
    \ No newline at end of file
    +[45629c5]
    \ No newline at end of file
    diff --git a/posts/useful/index.html b/posts/useful/index.html
    index e72004d..c891539 100644
    --- a/posts/useful/index.html
    +++ b/posts/useful/index.html
    @@ -9,4 +9,4 @@ One-minute read
    • [6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/vattention/index.html b/posts/vattention/index.html index 56a1e47..394c45f 100644 --- a/posts/vattention/index.html +++ b/posts/vattention/index.html @@ -31,4 +31,4 @@ The GPU TLB hierarchy is sensitive to page sizes.

      • 4KB Pages:< 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/posts/vibe-coding-from-the-jeep/index.html b/posts/vibe-coding-from-the-jeep/index.html index 5d55ec8..690dad4 100644 --- a/posts/vibe-coding-from-the-jeep/index.html +++ b/posts/vibe-coding-from-the-jeep/index.html @@ -12,17 +12,17 @@ Growing up in China before the internet age, my window to the outside world was Link to heading

        The first hurdle was mundane but blocking: My Bluetooth keyboard wouldn’t pair. The head unit could see other devices, but refused to connect to my keyboard.

        Attempt 1: The USB Dongle Bypass -Link to heading

        My first instinct was to blame the cheap Chinese head unit hardware. I grabbed a spare TP-Link USB Bluetooth dongle and plugged it in, hoping to bypass the internal stack entirely.

        The device showed up in lsusb, but it remained inert. A quick check of the kernel config via zcat /proc/config.gz revealed the bad news:

        # CONFIG_BT is not set
        +Link to heading

        My first instinct was to blame the cheap Chinese head unit hardware. I grabbed a spare TP-Link USB Bluetooth dongle and plugged it in, hoping to bypass the internal stack entirely.

        The device showed up in lsusb, but it remained inert. A quick check of the kernel config via zcat /proc/config.gz revealed the bad news:

        # CONFIG_BT is not set
         

        The kernel was compiled without generic Bluetooth driver support (btusb). Even with root access, I couldn’t load the drivers because they simply didn’t exist in the firmware. I was stuck with the internal hardware.

        Attempt 2: The “Dual Bluetooth” Fix -Link to heading

        Forced back to the built-in Bluetooth, I tried to diagnose why it was ignoring my keyboard. Standard debugging tools painted a grim picture:

        ❯ hciconfig -a
        +Link to heading

        Forced back to the built-in Bluetooth, I tried to diagnose why it was ignoring my keyboard. Standard debugging tools painted a grim picture:

        ❯ hciconfig -a
         # (Empty output - no standard HCI interface found)
         
         ❯ ps -A | grep -iE "goc|ivt|syu"
         u0_a50 3456 ... com.goc.sdk  # Accessing the proprietary BT chip
         

        The diagnosis was clear: The internal Bluetooth chip is acting in Slave Mode (Client), managed by a proprietary com.goc.sdk service instead of the standard Android Bluetooth stack. It’s designed to be a speaker for your phone, not to host a keyboard.

        The Fix: Hidden deep in the Factory Settings (password 8888), there’s a toggle called “Dual Bluetooth”. Enabling this flips the proprietary stack to expose a standard Host interface. Enable that, and suddenly my mechanical keyboard connected instantly.

        The Software: Termux + Claude -Link to heading

        With input sorted, the software setup was surprisingly straightforward. Termux was the obvious choice for a terminal.

        I discovered that Claude Code works on Termux with zero hassle.

        The setup was shockingly simple:

        pkg install nodejs git ripgrep
        +Link to heading

        With input sorted, the software setup was surprisingly straightforward. Termux was the obvious choice for a terminal.

        I discovered that Claude Code works on Termux with zero hassle.

        The setup was shockingly simple:

        pkg install nodejs git ripgrep
         npm install -g @anthropic-ai/claude-code
         

        Authentication via claude login worked out of the box. Now, I have a fully capable coding agent running directly on my dashboard. I can pull a repo, ask Claude to refactor a module, and push the changes—all without opening a laptop.

        S3 File

        Key Insights @@ -32,4 +32,4 @@ Growing up in China before the internet age, my window to the outside world was 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/series/index.html b/series/index.html index 51c0896..ba7b7c9 100644 --- a/series/index.html +++ b/series/index.html @@ -4,4 +4,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index f719b98..0679149 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://ericxliu.me/2026-01-22T06:48:07+00:00weekly0.5https://ericxliu.me/posts/vibe-coding-from-the-jeep/2026-01-22T06:48:07+00:00weekly0.5https://ericxliu.me/posts/2026-01-22T06:48:07+00:00weekly0.5https://ericxliu.me/posts/reverse-engineering-antigravity-ide/2026-01-22T01:49:53+00:00weekly0.5https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/2026-01-08T06:02:38+00:00weekly0.5https://ericxliu.me/posts/debugging-authentik-performance/2026-01-03T06:57:12+00:00weekly0.5https://ericxliu.me/posts/open-webui-openai-websearch/2025-12-29T07:15:58+00:00weekly0.5https://ericxliu.me/posts/technical-deep-dive-llm-categorization/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/about/2025-12-20T09:52:07-08:00weekly0.5https://ericxliu.me/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/2025-12-19T21:21:55+00:00weekly0.5https://ericxliu.me/posts/vattention/2025-12-19T21:21:55+00:00weekly0.5https://ericxliu.me/posts/jellyfin-sso-with-authentik/2025-12-28T21:21:42+00:00weekly0.5https://ericxliu.me/posts/benchmarking-llms-on-jetson-orin-nano/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/flashing-jetson-orin-nano-in-virtualized-environments/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/openwrt-mwan3-wireguard-endpoint-exclusion/2025-10-02T08:34:05+00:00weekly0.5https://ericxliu.me/posts/unifi-vlan-migration-to-zone-based-architecture/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/quantization-in-llms/2025-08-20T06:02:35+00:00weekly0.5https://ericxliu.me/posts/breville-barista-pro-maintenance/2025-08-20T06:04:36+00:00weekly0.5https://ericxliu.me/posts/secure-boot-dkms-and-mok-on-proxmox-debian/2025-08-14T06:50:22+00:00weekly0.5https://ericxliu.me/posts/how-rvq-teaches-llms-to-see-and-hear/2025-08-08T17:36:52+00:00weekly0.5https://ericxliu.me/posts/supabase-deep-dive/2025-08-04T03:59:37+00:00weekly0.5https://ericxliu.me/posts/ppo-for-language-models/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/2025-08-03T06:02:48+00:00weekly0.5https://ericxliu.me/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/2025-08-03T03:41:10+00:00weekly0.5https://ericxliu.me/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/2025-08-03T04:20:20+00:00weekly0.5https://ericxliu.me/posts/transformer-s-core-mechanics/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/useful/2025-08-03T08:37:28-07:00weekly0.5https://ericxliu.me/authors/weekly0.5https://ericxliu.me/categories/weekly0.5https://ericxliu.me/series/weekly0.5https://ericxliu.me/tags/weekly0.5 \ No newline at end of file +https://ericxliu.me/posts/blog-draft/2026-02-04T06:18:45+00:00weekly0.5https://ericxliu.me/2026-02-04T06:18:45+00:00weekly0.5https://ericxliu.me/posts/2026-02-04T06:18:45+00:00weekly0.5https://ericxliu.me/posts/vibe-coding-from-the-jeep/2026-01-22T06:48:07+00:00weekly0.5https://ericxliu.me/posts/reverse-engineering-antigravity-ide/2026-01-22T01:49:53+00:00weekly0.5https://ericxliu.me/posts/rooting-pixel-2-xl-for-reverse-engineering/2026-01-08T06:02:38+00:00weekly0.5https://ericxliu.me/posts/debugging-authentik-performance/2026-01-03T06:57:12+00:00weekly0.5https://ericxliu.me/posts/open-webui-openai-websearch/2025-12-29T07:15:58+00:00weekly0.5https://ericxliu.me/posts/technical-deep-dive-llm-categorization/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/about/2025-12-20T09:52:07-08:00weekly0.5https://ericxliu.me/posts/the-convergence-of-fast-weights-linear-attention-and-state-space-models/2025-12-19T21:21:55+00:00weekly0.5https://ericxliu.me/posts/vattention/2025-12-19T21:21:55+00:00weekly0.5https://ericxliu.me/posts/jellyfin-sso-with-authentik/2025-12-28T21:21:42+00:00weekly0.5https://ericxliu.me/posts/benchmarking-llms-on-jetson-orin-nano/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/flashing-jetson-orin-nano-in-virtualized-environments/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/openwrt-mwan3-wireguard-endpoint-exclusion/2025-10-02T08:34:05+00:00weekly0.5https://ericxliu.me/posts/unifi-vlan-migration-to-zone-based-architecture/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/quantization-in-llms/2025-08-20T06:02:35+00:00weekly0.5https://ericxliu.me/posts/breville-barista-pro-maintenance/2025-08-20T06:04:36+00:00weekly0.5https://ericxliu.me/posts/secure-boot-dkms-and-mok-on-proxmox-debian/2025-08-14T06:50:22+00:00weekly0.5https://ericxliu.me/posts/how-rvq-teaches-llms-to-see-and-hear/2025-08-08T17:36:52+00:00weekly0.5https://ericxliu.me/posts/supabase-deep-dive/2025-08-04T03:59:37+00:00weekly0.5https://ericxliu.me/posts/ppo-for-language-models/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/mixture-of-experts-moe-models-challenges-solutions-in-practice/2025-08-03T06:02:48+00:00weekly0.5https://ericxliu.me/posts/t5-the-transformer-that-zigged-when-others-zagged-an-architectural-deep-dive/2025-08-03T03:41:10+00:00weekly0.5https://ericxliu.me/posts/espresso-theory-application-a-guide-for-the-breville-barista-pro/2025-08-03T04:20:20+00:00weekly0.5https://ericxliu.me/posts/transformer-s-core-mechanics/2026-01-10T20:10:48+00:00weekly0.5https://ericxliu.me/posts/useful/2025-08-03T08:37:28-07:00weekly0.5https://ericxliu.me/authors/weekly0.5https://ericxliu.me/categories/weekly0.5https://ericxliu.me/series/weekly0.5https://ericxliu.me/tags/weekly0.5 \ No newline at end of file diff --git a/tags/index.html b/tags/index.html index a2b67a0..044535c 100644 --- a/tags/index.html +++ b/tags/index.html @@ -4,4 +4,4 @@ 2016 - 2026 Eric X. Liu -[6100dca] \ No newline at end of file +[45629c5] \ No newline at end of file