<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Anh]]></title><description><![CDATA[Building things that may or may not break in prod. I code, therefore I am.]]></description><link>https://blog.anh.sh</link><image><url>https://cdn.hashnode.com/uploads/logos/69d46ed9d0d885189663df0a/1dd1e5a3-f125-472b-b63a-cd142d8c7ae8.jpg</url><title>Anh</title><link>https://blog.anh.sh</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 07 Apr 2026 15:21:35 GMT</lastBuildDate><atom:link href="https://blog.anh.sh/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Why (and How) I Built a Go AI SDK]]></title><description><![CDATA[GoAI, a Go (Golang) LLM library: 22+ providers, 2 dependencies, type-safe generics. v0.6.1, Go 1.25+. I built it to learn Go by adding AI to infrastructure that already runs on Go.


The Go AI SDK lan]]></description><link>https://blog.anh.sh/why-and-how-i-built-a-go-ai-sdk</link><guid isPermaLink="true">https://blog.anh.sh/why-and-how-i-built-a-go-ai-sdk</guid><category><![CDATA[Go Language]]></category><category><![CDATA[golang]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[sdk]]></category><category><![CDATA[openai]]></category><category><![CDATA[#anthropic]]></category><category><![CDATA[gemini]]></category><category><![CDATA[Amazon Bedrock]]></category><category><![CDATA[mcp]]></category><category><![CDATA[streaming]]></category><category><![CDATA[generics]]></category><category><![CDATA[tool calling]]></category><category><![CDATA[StructuredOutput]]></category><category><![CDATA[supplychainsecurity]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[anh]]></dc:creator><pubDate>Tue, 07 Apr 2026 13:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d46ed9d0d885189663df0a/0729151a-ef24-4e44-9a7b-011d11d925b4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><a href="https://goai.sh">GoAI</a>, a Go (Golang) LLM library: 22+ providers, 2 dependencies, type-safe generics. v0.6.1, Go 1.25+. I built it to learn Go by adding AI to infrastructure that already runs on Go.</p>
</blockquote>
<hr />
<h2>The Go AI SDK landscape</h2>
<p>Python has LangChain, LlamaIndex, LiteLLM. TypeScript has the Vercel AI SDK. Go has options, but none covered all the bases.</p>
<h3>What I found</h3>
<table>
<thead>
<tr>
<th>Library</th>
<th>Stars</th>
<th>Created</th>
<th>Providers</th>
<th>What's missing</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://github.com/sashabaranov/go-openai"><strong>go-openai</strong></a></td>
<td>~10.6k</td>
<td>Aug 2020</td>
<td>1</td>
<td>Single provider only</td>
</tr>
<tr>
<td><a href="https://github.com/cloudwego/eino"><strong>Eino</strong></a> (ByteDance)</td>
<td>~10.5k</td>
<td>Dec 2024</td>
<td>10</td>
<td>Graph framework, different scope</td>
</tr>
<tr>
<td><a href="https://github.com/tmc/langchaingo"><strong>LangChainGo</strong></a></td>
<td>~9k</td>
<td>Feb 2023</td>
<td>~14</td>
<td>170+ deps, no MCP, no generics schema</td>
</tr>
<tr>
<td><a href="https://github.com/google/adk-go"><strong>Google ADK Go</strong></a></td>
<td>~7.5k</td>
<td>May 2025</td>
<td>Gemini-first</td>
<td>Agent framework, Gemini-optimized</td>
</tr>
<tr>
<td><a href="https://github.com/firebase/genkit"><strong>Genkit Go</strong></a> (Google)</td>
<td>~5.7k*</td>
<td>2024</td>
<td>~6</td>
<td>Google Cloud-heavy, 129 deps</td>
</tr>
<tr>
<td><a href="https://github.com/openai/openai-go"><strong>openai-go</strong></a></td>
<td>~3.1k</td>
<td>Jul 2024</td>
<td>1+Azure</td>
<td>Single provider by design</td>
</tr>
<tr>
<td><a href="https://github.com/anthropics/anthropic-sdk-go"><strong>anthropic-sdk-go</strong></a></td>
<td>~960</td>
<td>Jul 2024</td>
<td>1+Bedrock</td>
<td>Single provider by design</td>
</tr>
<tr>
<td><a href="https://github.com/teilomillet/gollm"><strong>gollm</strong></a></td>
<td>~570</td>
<td>Jul 2024</td>
<td>6</td>
<td>Limited tool calling, limited streaming</td>
</tr>
<tr>
<td><a href="https://github.com/jetify-com/ai"><strong>Jetify AI</strong></a></td>
<td>~230</td>
<td>May 2025</td>
<td>2</td>
<td>Early stage, 2 providers</td>
</tr>
<tr>
<td><a href="https://github.com/jxnl/instructor-go"><strong>instructor-go</strong></a></td>
<td>~200</td>
<td>May 2024</td>
<td>4</td>
<td>Structured output only</td>
</tr>
<tr>
<td><a href="https://github.com/zendev-sh/goai"><strong>GoAI</strong></a></td>
<td>new</td>
<td>Mar 2026</td>
<td>22+</td>
<td>2 deps, this post</td>
</tr>
</tbody></table>
<p>*Genkit stars shared across JS, Go, and Python.</p>
<p>The gaps I kept running into:</p>
<ul>
<li><p><strong>No</strong> <code>SchemaFrom[T]</code>: generate JSON Schema from Go structs using generics. Only Genkit Go has this, but pulls in 129 dependencies</p>
</li>
<li><p><strong>No built-in MCP</strong>: <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> connects to external tool servers (filesystem, GitHub, databases, Kubernetes APIs). For infra and edge use-cases, this is how agents interact with surrounding systems</p>
</li>
<li><p><strong>No provider-defined tools</strong>: OpenAI has web search, code interpreter, file search. Anthropic has computer use, bash, text editor, web fetch, code execution. Google has Google Search, URL context, code execution. None of the Go LLM libraries expose these</p>
</li>
<li><p><strong>No prompt caching</strong>: Anthropic and OpenAI support cache control to reduce cost and latency</p>
</li>
<li><p><strong>No streaming structured output</strong>: <code>StreamObject[T]</code> that progressively populates a Go struct as JSON arrives</p>
</li>
<li><p><strong>Dependency weight</strong>: a library that handles API keys to every major AI provider is a high-value target. Fewer dependencies, smaller attack surface</p>
</li>
</ul>
<h2>Why I built GoAI</h2>
<p><strong>1. To learn Go.</strong> Not a Go expert. Built this to learn by solving a real problem. PRs welcome.</p>
<p><strong>2. To learn AI-assisted development.</strong> Designed by me, built with Claude Code. I'll write a separate post on the workflow, what worked, and what didn't.</p>
<p><strong>3. To build a foundation for agent orchestration.</strong> Lightweight AI agents in CI/CD, Kubernetes, CLI, edge. GoAI is the foundation layer.</p>
<hr />
<h2>What I learned from the Vercel AI SDK</h2>
<p>The <a href="https://ai-sdk.dev/">Vercel AI SDK</a> is the reference. GoAI's API surface is directly inspired by it:</p>
<pre><code class="language-go">goai.GenerateText(ctx, model, opts...)
goai.StreamText(ctx, model, opts...)
goai.GenerateObject[T](ctx, model, opts...)
goai.StreamObject[T](ctx, model, opts...)
goai.Embed(ctx, model, value, opts...)
goai.EmbedMany(ctx, model, values, opts...)
goai.GenerateImage(ctx, model, opts...)
</code></pre>
<p>What I took from Vercel:</p>
<ul>
<li><p><strong>Unified API surface</strong> across all providers</p>
</li>
<li><p><strong>Minimal provider interface</strong>, 2-3 methods per provider, SDK handles the rest</p>
</li>
<li><p><strong>Tool loop with MaxSteps</strong>, model calls tool, execute, feed back, repeat</p>
</li>
<li><p><strong>Streaming-first</strong></p>
</li>
<li><p><strong>Retry with exponential backoff</strong> and Retry-After header awareness</p>
</li>
</ul>
<h2>Go features that solved real problems</h2>
<p><strong>Go generics for type-safe LLM output.</strong> <code>GenerateObject[T]()</code> returns <code>ObjectResult[T]</code> where <code>.Object</code> is <code>T</code>, not <code>interface{}</code>. <code>SchemaFrom[T]()</code> walks the struct via <code>reflect</code> to generate JSON Schema, with cycle detection, embedded struct flattening, and nullable pointer fields. No schema files, no codegen. <a href="https://goai.sh/getting-started/structured-output">Docs</a>.</p>
<pre><code class="language-go">type Recipe struct {
    Name        string   `json:"name"`
    Ingredients []string `json:"ingredients"`
    Steps       []string `json:"steps"`
}

result, err := goai.GenerateObject[Recipe](ctx, model,
    goai.WithPrompt("A simple pasta recipe"),
)
// result.Object is Recipe, fully typed
</code></pre>
<p><strong>Functional options for composable configuration.</strong> <code>WithX()</code> pattern gives composability and extensibility without breaking changes. Separate option types (<code>Option</code> vs <code>ImageOption</code>) so the compiler catches misuse. Options are composable via <code>WithOptions()</code>. <a href="https://goai.sh/api/options">Docs</a>.</p>
<pre><code class="language-go">result, err := goai.GenerateText(ctx, model,
    goai.WithSystem("You are a helpful assistant"),
    goai.WithPrompt("Hello"),
    goai.WithMaxSteps(3),
    goai.WithTools(searchTool, calculatorTool),
    goai.WithMaxRetries(2),
    goai.WithOnResponse(func(info goai.ResponseInfo) {
        log.Printf("latency=%v tokens=%d", info.Latency, info.Usage.TotalTokens)
    }),
)
</code></pre>
<p><strong>Interfaces for provider abstraction.</strong> Three interfaces (<code>LanguageModel</code>, <code>EmbeddingModel</code>, <code>ImageModel</code>), implicitly satisfied. 22+ providers conform independently. Adding a provider never touches core. <a href="https://goai.sh/providers">Docs</a>.</p>
<p><strong>Goroutines and channels for streaming.</strong> Background goroutine consumes the provider stream (SSE for most providers, binary EventStream for Bedrock, NDJSON for Gemini). Callers read from <code>&lt;-chan string</code>. All functions are context-aware, retries respect <code>ctx.Done()</code>. Tool loops execute in parallel with bounded concurrency via semaphores. <a href="https://goai.sh/concepts/streaming">Docs</a>.</p>
<pre><code class="language-go">stream, _ := goai.StreamText(ctx, model, goai.WithPrompt("Tell me a story"))

for text := range stream.TextStream() {
    fmt.Print(text)
}

if err := stream.Err(); err != nil {
    log.Fatal(err)
}
</code></pre>
<p><code>sync.Map</code> <strong>and</strong> <code>sync.Once</code><strong>.</strong> Schema generation cached in <code>sync.Map</code> by type. Stream consumption uses <code>sync.Once</code> to start the internal goroutine exactly once.</p>
<p><code>errors.As</code> <strong>for cross-provider errors.</strong> <code>APIError</code> and <code>ContextOverflowError</code> defined once, every provider wraps into these. <code>errors.As(err, &amp;apiErr)</code> works through any wrapping depth. <a href="https://goai.sh/api/errors">Docs</a>.</p>
<p><code>internal/</code> <strong>for encapsulation.</strong> The <code>openaicompat</code> codec lives in <code>internal/</code>, shared across 13+ providers but invisible to users.</p>
<h2>Fewer dependencies, smaller attack surface</h2>
<p>GoAI's core module has <strong>2 dependencies</strong>:</p>
<ul>
<li><p>Direct: <code>golang.org/x/oauth2</code></p>
</li>
<li><p>Indirect: <code>cloud.google.com/go/compute/metadata</code></p>
</li>
</ul>
<p>No HTTP frameworks, no JSON schema libraries, no third-party provider SDKs. Raw HTTP calls, parse responses directly.</p>
<p>This was a design choice from day one (mid-March 2026). For context on why this matters:</p>
<ul>
<li><p><a href="https://github.com/BerriAI/litellm">LiteLLM</a> (Python, 95M monthly downloads): <a href="https://www.herodevs.com/blog-posts/the-litellm-supply-chain-attack-what-happened-why-it-matters-and-what-to-do-next">compromised</a>, malicious versions harvested API keys and cloud credentials. 40,000+ downloads in 40 minutes.</p>
</li>
<li><p><a href="https://github.com/axios/axios">Axios</a> (npm, 100M+ weekly downloads): <a href="https://www.microsoft.com/en-us/security/blog/2026/04/01/mitigating-the-axios-npm-supply-chain-compromise/">compromised by a North Korean state actor</a>, cross-platform RAT via fake dependency.</p>
</li>
</ul>
<p>A multi-provider AI SDK concentrates API keys for every provider. Every dependency is an attack vector.</p>
<pre><code class="language-plaintext">GoAI core:        2 dependencies
Eino:            37 dependencies
instructor-go:   41 dependencies
Genkit Go:      129 dependencies
LangChainGo:    170+ dependencies
</code></pre>
<p>Both Langfuse and OpenTelemetry live in separate <code>go.mod</code> submodules. Go's <a href="https://go.dev/ref/mod#go-sum-files"><code>go.sum</code></a> checksum verification and <a href="https://go.dev/ref/mod#module-proxy">GOPROXY</a> transparency log help too.</p>
<hr />
<h2>Architecture</h2>
<img src="https://cdn.hashnode.com/uploads/covers/69d46ed9d0d885189663df0a/812ef8dc-2d14-41d6-9438-ca391a1c8121.png" alt="" style="display:block;margin:0 auto" />

<h3>Three layers:</h3>
<ul>
<li><p><strong>User-facing API</strong> (<code>generate.go</code>, <code>object.go</code>, <code>embed.go</code>, <code>image.go</code>): seven functions, options parsing, retry, caching, tool loops, hooks. Context-aware. Multimodal input (<code>PartImage</code>, <code>PartFile</code>). Token usage tracking. <a href="https://goai.sh/api/core-functions">Docs</a>.</p>
</li>
<li><p><strong>Provider interface</strong> (<code>provider/provider.go</code>): three interfaces, minimal surface, easy to mock. <a href="https://goai.sh/providers">Docs</a>.</p>
</li>
<li><p><strong>Provider implementations</strong> (<code>provider/openai/</code>, <code>provider/anthropic/</code>, etc.): 22+ providers, separate packages. Import only what you use.</p>
</li>
</ul>
<pre><code class="language-go">type LanguageModel interface {
    ModelID() string
    DoGenerate(ctx context.Context, params GenerateParams) (*GenerateResult, error)
    DoStream(ctx context.Context, params GenerateParams) (*StreamResult, error)
}

type EmbeddingModel interface {
    ModelID() string
    DoEmbed(ctx context.Context, values []string, params EmbedParams) (*EmbedResult, error)
    MaxValuesPerCall() int
}

type ImageModel interface {
    ModelID() string
    DoGenerate(ctx context.Context, params ImageParams) (*ImageResult, error)
}
</code></pre>
<p>Providers implement <code>DoGenerate</code> and <code>DoStream</code>. GoAI handles retries, caching, tool execution, streaming, hooks.</p>
<h3>The shared codec</h3>
<img src="https://cdn.hashnode.com/uploads/covers/69d46ed9d0d885189663df0a/edd6a888-730b-4c8b-8b36-f57af4297ac8.png" alt="" style="display:block;margin:0 auto" />

<p>13+ providers share a single codec in <code>internal/openaicompat/</code> (BuildRequest, ParseStream, ParseResponse). Providers with unique wire formats (Anthropic Messages API, Google Gemini REST, AWS Bedrock Converse, Cohere Chat v2) have their own implementations. Azure, vLLM, and MiniMax delegate through existing providers. <a href="https://goai.sh/providers">Docs</a>.</p>
<h3>Tool calling</h3>
<img src="https://cdn.hashnode.com/uploads/covers/69d46ed9d0d885189663df0a/63c357b7-b288-48d0-bbfb-5808f0681a73.png" alt="" style="display:block;margin:0 auto" />

<p>Two kinds: <strong>user-defined</strong> (you write <code>Execute</code> with <code>json.RawMessage</code> input) and <strong>provider-defined</strong> (runs on provider infrastructure). User-defined tools execute in parallel between steps. <a href="https://goai.sh/concepts/tools">Docs</a>.</p>
<h3>Provider-defined tools</h3>
<p>Providers expose built-in tools (web search, code execution, computer use). Each returns a <code>provider.ToolDefinition</code> that you wrap into <code>goai.Tool</code>. <a href="https://goai.sh/concepts/provider-tools">Docs</a>.</p>
<pre><code class="language-go">// Get the provider tool definition
def := anthropic.Tools.WebSearch(anthropic.WithMaxUses(5))

// Wrap into goai.Tool (provider-defined tools have no Execute func)
tools := []goai.Tool{{
    Name:                   def.Name,
    ProviderDefinedType:    def.ProviderDefinedType,
    ProviderDefinedOptions: def.ProviderDefinedOptions,
}}

result, _ := goai.GenerateText(ctx, model,
    goai.WithPrompt("Search for Go AI libraries"),
    goai.WithTools(tools...),
)
</code></pre>
<p>Available: Anthropic (10: Computer, Bash, TextEditor, WebSearch, WebFetch, CodeExecution + versioned variants), OpenAI (4: WebSearch, CodeInterpreter, FileSearch, ImageGeneration), Google (3: GoogleSearch, URLContext, CodeExecution), xAI (2), Groq (1).</p>
<h3>Go MCP client</h3>
<p>Built-in MCP client. <a href="https://goai.sh/concepts/mcp">Docs</a>.</p>
<pre><code class="language-go">transport := mcp.NewStdioTransport("npx", []string{"@modelcontextprotocol/server-filesystem", "/tmp"})
client := mcp.NewClient("myapp", "1.0", mcp.WithTransport(transport))
client.Connect(ctx)
defer client.Close()

mcpTools, _ := client.ListTools(ctx, nil)
tools := mcp.ConvertTools(client, mcpTools.Tools)

result, _ := goai.GenerateText(ctx, model,
    goai.WithPrompt("List files in /tmp"),
    goai.WithTools(tools...),
    goai.WithMaxSteps(3),
)
</code></pre>
<p>Stdio, SSE, and HTTP transports.</p>
<h3>Observability</h3>
<ul>
<li><p><a href="https://goai.sh/concepts/observability"><strong>Langfuse</strong></a>: trace-based observability, token counting, error tracking (separate <code>go.mod</code>, contributed by <a href="https://github.com/oscarbc96">@oscarbc96</a> in <a href="https://github.com/zendev-sh/goai/pull/24">#24</a>)</p>
</li>
<li><p><a href="https://goai.sh/concepts/observability"><strong>OpenTelemetry</strong></a>: distributed tracing and metrics (separate <code>go.mod</code>)</p>
</li>
</ul>
<p>Both hook into <code>OnRequest</code>, <code>OnResponse</code>, <code>OnToolCall</code>, <code>OnToolCallStart</code>, <code>OnStepFinish</code>.</p>
<p>Also supported:</p>
<ul>
<li><p><a href="https://goai.sh/concepts/prompt-caching">Prompt caching</a> via <code>WithPromptCaching()</code>, implements <a href="https://arxiv.org/abs/2601.06007v2">arxiv 2601.06007v2</a>: cache system prompts only (41-80% cost, 13-31% latency savings for agentic workloads). Cache token tracking normalized across Anthropic, OpenAI, Google, Bedrock</p>
</li>
<li><p>Reasoning tokens, supported for models that expose them</p>
</li>
<li><p>Citations, <code>result.Sources</code> for providers that return source annotations</p>
</li>
<li><p>Auto-batched embeddings, <code>EmbedMany</code> with bounded parallelism</p>
</li>
<li><p><a href="https://goai.sh/concepts/tools"><code>WithToolChoice</code></a>, <code>WithTimeout</code>, <code>WithProviderOptions</code></p>
</li>
<li><p><code>ToolCallIDFromContext</code> for execution tracing</p>
</li>
<li><p><a href="https://goai.sh/providers/compat"><code>compat</code> provider</a> for any OpenAI-compatible endpoint</p>
</li>
<li><p>90%+ test coverage with mock HTTP servers</p>
</li>
</ul>
<p><a href="https://goai.sh">Full docs</a>.</p>
<hr />
<h2>Getting started</h2>
<pre><code class="language-bash">go get github.com/zendev-sh/goai
</code></pre>
<pre><code class="language-go">model := openai.Chat("gpt-4o")

result, _ := goai.GenerateText(ctx, model,
    goai.WithPrompt("Explain Go interfaces in 3 sentences"),
)
fmt.Println(result.Text)
</code></pre>
<p>Switch providers, same code:</p>
<pre><code class="language-go">model := anthropic.Chat("claude-sonnet-4-20250514")
model := google.Chat("gemini-2.0-flash")
model := bedrock.Chat("anthropic.claude-sonnet-4-20250514-v1:0")
model := ollama.Chat("llama3")
model := compat.Chat("my-model", compat.WithBaseURL("https://my-api.com/v1"))
</code></pre>
<p>More: <a href="https://goai.sh/getting-started/structured-output">structured output</a>, <a href="https://goai.sh/concepts/streaming">streaming</a>, <a href="https://goai.sh/concepts/tools">tool calling</a>, <a href="https://goai.sh/concepts/mcp">MCP</a>, <a href="https://goai.sh/api/core-functions">embeddings</a>, <a href="https://goai.sh/api/core-functions">image generation</a>.</p>
<hr />
<h2>Benchmarks</h2>
<p>Apple M2, 3 runs, in-process mock servers, identical SSE fixtures (50KB payload). <a href="https://github.com/zendev-sh/goai/tree/main/bench">Source</a>.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>GoAI</th>
<th>Vercel AI SDK</th>
<th>Delta</th>
</tr>
</thead>
<tbody><tr>
<td>Cold start</td>
<td>569us</td>
<td>13.89ms</td>
<td>24x</td>
</tr>
<tr>
<td>Time to first chunk</td>
<td>320us</td>
<td>412us</td>
<td>1.3x</td>
</tr>
<tr>
<td>Streaming throughput</td>
<td>1.46ms/op</td>
<td>1.62ms/op</td>
<td>1.1x</td>
</tr>
<tr>
<td>GenerateText</td>
<td>55.7us/op</td>
<td>79.0us/op</td>
<td>1.4x</td>
</tr>
<tr>
<td>Memory (1 stream)</td>
<td>220KB</td>
<td>676KB</td>
<td>3x less</td>
</tr>
<tr>
<td>Schema generation</td>
<td>3.6us/op</td>
<td>3.5us/op</td>
<td>~parity</td>
</tr>
</tbody></table>
<hr />
<h2>What's next</h2>
<p>First commit was March 18. 18 releases in 3 weeks, now at v0.6.1. The SDK covers the provider layer: unified API, streaming, tool calling, structured output, MCP, observability. Next up is an agent orchestration layer on top of GoAI for CI/CD, Kubernetes, and CLI workflows.</p>
<ul>
<li><p><a href="https://github.com/zendev-sh/goai">GitHub</a></p>
</li>
<li><p><a href="https://goai.sh">Docs</a></p>
</li>
<li><p><a href="https://goai.sh/examples">Examples</a></p>
</li>
<li><p><a href="https://github.com/zendev-sh/goai/issues">Issues</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>