n4nAI

A Go CLI for comparing Gemini 3 and Llama 4 outputs side by side

Build a Go CLI to send one prompt to Gemini 3 and Llama 4 and print responses side by side via an OpenAI-compatible gateway, with full runnable code.

n4n Team2 min read544 words

Audio narration

Coming soon — every post will get a voice note here.

Building a go cli compare gemini 3 llama 4 tool is the fastest way to eyeball differences in tone, reasoning, and formatting between two very different model families. This guide walks through a small, dependency-light Go program that fires one prompt at both models through a single OpenAI-compatible endpoint and renders their outputs in parallel columns.

Step 1: Scaffold the Go module

Start in an empty directory and initialize a module. We avoid external dependencies entirely; the standard library is enough for HTTP, JSON, and CLI flags.

mkdir gemini-llama-cmp && cd gemini-llama-cmp
go mod init github.com/you/gemini-llama-cmp

You should see a go.mod file with a single module line and a Go version directive. That is the only scaffolding required.

Step 2: Define request/response types and config

The OpenAI-compatible chat completions contract is stable and simple. We declare minimal structs that match the fields we actually use. Adding the usage field lets us surface token counts later without another network call.

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

type msg struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type chatReq struct {
	Model       string `json:"model"`
	Messages    []msg  `json:"messages"`
	Temperature float32 `json:"temperature,omitempty"`
}

type chatResp struct {
	Choices []struct {
		Message msg `json:"message"`
	} `json:"choices"`
	Usage struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
	} `json:"usage"`
}

Configuration comes from the environment so we never hard-code secrets. The endpoint is the only other piece; a gateway like n4n.ai exposes one OpenAI-compatible endpoint that addresses 240+ models, so you point both calls at the same base URL and just change the model field.

Step 3: Implement the model caller

Write a single function that posts a prompt to a given model and returns the text. Set a timeout via context.WithTimeout so a hung provider does not block the comparison indefinitely.

func complete(ctx context.Context, baseURL, apiKey, model, prompt string) (string, int, error) {
	reqBody := chatReq{
		Model:       model,
		Messages:    []msg{{Role: "user", Content: prompt}},
		Temperature: 0.7,
	}
	b, err := json.Marshal(reqBody)
	if err != nil {
		return "", 0, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		baseURL+"/chat/completions", bytes.NewReader(b))
	if err != nil {
		return "", 0, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", 0, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", 0, fmt.Errorf("model %s: unexpected status %d", model, resp.StatusCode)
	}
	var cr chatResp
	if err := json.NewDecoder(resp.Body).Decode(&cr); err != nil {
		return "", 0, err
	}
	if len(cr.Choices) == 0 {
		return "", 0, fmt.Errorf("model %s: empty choices", model)
	}
	return cr.Choices[0].Message.Content, cr.Usage.CompletionTokens, nil
}

The function returns the completion text and token count. Because the gateway provides per-token usage metering, you can log those counts without extra bookkeeping.

Step 4: Run both calls concurrently

Sequential calls double latency. Use errgroup from the standard extension package or a bare sync.WaitGroup. Below is the wait-group version to keep the import surface minimal.

func runComparison(baseURL, apiKey, prompt string) (string, string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
	defer cancel()

	var geminiOut, llamaOut string
	var geminiErr, llamaErr error
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		defer wg.Done()
		geminiOut, _, geminiErr = complete(ctx, baseURL, apiKey, "google/gemini-3", prompt)
	}()
	go func() {
		defer wg.Done()
		llamaOut, _, llamaErr = complete(ctx, baseURL, apiKey, "meta/llama-4", prompt)
	}()
	wg.Wait()

	if geminiErr != nil {
		return "", "", fmt.Errorf("gemini: %w", geminiErr)
	}
	if llamaErr != nil {
		return "", "", fmt.Errorf("llama: %w", llamaErr)
	}
	return geminiOut, llamaOut, nil
}

Pin the model names exactly. The gateway honors client routing directives, so a request for google/gemini-3 will not silently fall back to another model—critical when your goal is a strict go cli compare gemini 3 llama 4 evaluation.

Step 5: Render side by side

A naive fmt.Printf with two %s placeholders breaks on multi-line responses. Use text/tabwriter and split on newlines so each line pair aligns.

import (
	"strings"
	"text/tabwriter"
)

func printSideBySide(a, b string) {
	aLines := strings.Split(a, "\n")
	bLines := strings.Split(b, "\n")
	n := len(aLines)
	if len(bLines) > n {
		n = len(bLines)
	}
	w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
	fmt.Fprintln(w, "GEMINI 3\tLLAMA 4")
	for i := 0; i < n; i++ {
		left := ""
		if i < len(aLines) {
			left = aLines[i]
		}
		right := ""
		if i < len(bLines) {
			right = bLines[i]
		}
		fmt.Fprintf(w, "%s\t%s\n", left, right)
	}
	w.Flush()
}

Handling wide terminals

If a line exceeds 80 columns, the tab writer will still align but wrapping looks messy. For a production go cli compare gemini 3 llama 4 utility, pipe through less -S or implement rune-aware wrapping. For this how-to, line splitting is sufficient.

Step 6: Wire up flags and environment

The CLI should accept a prompt flag and read secrets from the environment. Default the endpoint to the gateway base URL; override for local testing.

func main() {
	prompt := flag.String("p", "", "prompt to send to both models")
	flag.Parse()
	if *prompt == "" {
		fmt.Fprintln(os.Stderr, "error: -p flag is required")
		os.Exit(2)
	}
	apiKey := os.Getenv("API_KEY")
	if apiKey == "" {
		fmt.Fprintln(os.Stderr, "error: API_KEY env var not set")
		os.Exit(2)
	}
	baseURL := os.Getenv("ENDPOINT")
	if baseURL == "" {
		baseURL = "https://api.n4n.ai/v1" // single OpenAI-compatible endpoint
	}

	gemini, llama, err := runComparison(baseURL, apiKey, *prompt)
	if err != nil {
		fmt.Fprintln(os.Stderr, "comparison failed:", err)
		os.Exit(1)
	}
	printSideBySide(gemini, llama)
}

Add import "flag" and import "sync" to the header block. The full file compiles as a single main.go.

Step 7: Build and verify success

Build the binary and run it against a real prompt.

go build -o cmp .
export API_KEY=sk-your-key-here
./cmp -p "Explain quantum entanglement in one sentence."

Verification criteria:

  • The process exits with code 0.
  • Two columns headed GEMINI 3 and LLAMA 4 appear, each containing non-empty text.
  • If you set a bogus API_KEY, you should see comparison failed: on stderr and exit code 1, not a panic.
  • Running ./cmp without -p prints the usage error and exits 2.

That is the core of the go cli compare gemini 3 llama 4 workflow. From here, extend the tool with a --temperature flag, JSON output mode for scripting, or a --tokens switch that prints the Usage field returned by each call. The concurrency pattern and strict model pinning remain unchanged.

Tagsgogemini-3llama-4cli

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All building cli tools for llm apis posts →