<?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>Thu, 14 May 2026 00:53:19 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[Anatomy of an OpenAI-compatible provider in Go]]></title><description><![CDATA[GoAI shipped Cloudflare Workers AI and FPT Smart Cloud providers in v0.7.0, then refactored the shared plumbing in v0.7.1. Chat-only providers come in at ~84 lines. The two new ones, with embeddings a]]></description><link>https://blog.anh.sh/anatomy-of-an-openai-compatible-provider-in-go</link><guid isPermaLink="true">https://blog.anh.sh/anatomy-of-an-openai-compatible-provider-in-go</guid><dc:creator><![CDATA[anh]]></dc:creator><pubDate>Sun, 19 Apr 2026 02:56:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69d46ed9d0d885189663df0a/26957922-d527-48f7-a7fc-7d0563c68d60.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><a href="https://goai.sh">GoAI</a> shipped <a href="https://developers.cloudflare.com/workers-ai/">Cloudflare Workers AI</a> and <a href="https://marketplace.fptcloud.com/">FPT Smart Cloud</a> providers in <a href="https://github.com/zendev-sh/goai/releases/tag/v0.7.0">v0.7.0</a>, then refactored the shared plumbing in <a href="https://github.com/zendev-sh/goai/releases/tag/v0.7.1">v0.7.1</a>. Chat-only providers come in at ~84 lines. The two new ones, with embeddings and unique routing, land at 126 and 132. This post walks through the anatomy and which Go features made it small.</p>
</blockquote>
<h2>Starting point</h2>
<p>OpenAI's Chat Completions and Embeddings shape is a de facto standard. Most inference vendors expose it. In GoAI, 18 of 24 providers speak this wire format. They differ only in URL, auth, and occasional routing. 14 of those share a single factory in <code>internal/openaicompat</code>. The other 4 are <a href="https://goai.sh/providers/openai"><code>openai</code></a> and <a href="https://goai.sh/providers/vertex"><code>vertex</code></a> with custom routing, plus <a href="https://goai.sh/providers/ollama"><code>ollama</code></a> and <a href="https://goai.sh/providers/vllm"><code>vllm</code></a> which wrap the generic <a href="https://goai.sh/providers/compat"><code>compat</code></a> provider.</p>
<p>"How much code for a new one?" About 84 lines for a chat-only provider. Most of that is options boilerplate users see in their IDE. Providers with embeddings or custom routing land in the 120s.</p>
<h2>The interface</h2>
<p>A provider implements two interfaces from <code>provider/</code>:</p>
<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
}
</code></pre>
<p>No base class, no registry, no lifecycle. Go's interfaces are satisfied implicitly, so adding a provider doesn't touch any other file.</p>
<h2>What's shared</h2>
<p><code>internal/openaicompat</code> owns the wire format and the HTTP plumbing. Two factories do most of the work:</p>
<pre><code class="language-go">func NewChatModel(cfg ChatModelConfig) provider.LanguageModel
func NewEmbeddingModel(cfg EmbeddingModelConfig) provider.EmbeddingModel
</code></pre>
<p>The factory handles request building, streaming, response parsing, token resolution, error dispatch, and the embedding round-trip. Provider packages fill in a config struct and pass it.</p>
<p><code>internal/</code> is a Go convention: packages under it are importable only within the same module tree, not by external consumers. That lets 14 providers (plus Ollama and vLLM via the <code>compat</code> wrapper) share the factory without exposing a new public API surface.</p>
<img alt="Provider anatomy: user code → provider package → shared factory → HTTP" style="display:block;margin:0 auto" />

<p>Provider packages stay thin and user-facing. The factory owns the plumbing. Two concrete providers show how this works.</p>
<h2>Cloudflare</h2>
<p>Cloudflare Workers AI is OpenAI-compatible, with one quirk: the URL embeds the account ID.</p>
<pre><code class="language-plaintext">https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions
</code></pre>
<p>The provider-specific work is URL construction. Everything else comes from the shared factory.</p>
<pre><code class="language-go">const defaultAPIBase = "https://api.cloudflare.com/client/v4"

func WithAccountID(id string) Option {
    return func(o *options) { o.accountID = id }
}

// In resolveOptions, after reading env vars CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID:
if o.baseURL == "" &amp;&amp; o.accountID != "" {
    o.baseURL = fmt.Sprintf("%s/accounts/%s/ai/v1", defaultAPIBase, o.accountID)
}
</code></pre>
<p>Usage:</p>
<pre><code class="language-go">model := cloudflare.Chat("@cf/meta/llama-3.1-8b-instruct",
    cloudflare.WithAccountID("your-account-id"))
</code></pre>
<p>Total file: 126 lines including chat, embeddings, and 6 <code>With*</code> options. <a href="https://goai.sh/providers/cloudflare">Cloudflare provider docs</a>.</p>
<h2>FPT Smart Cloud</h2>
<p>FPT Smart Cloud's AI marketplace has a different quirk: two regions, Global and Japan, each with its own model catalog.</p>
<pre><code class="language-go">const (
    baseURLGlobal = "https://mkp-api.fptcloud.com/v1"
    baseURLJP     = "https://mkp-api.fptcloud.jp/v1"
)

func WithRegion(region string) Option {
    return func(o *options) { o.region = region }
}

func regionBaseURL(region string) string {
    switch region {
    case "jp":
        return baseURLJP
    default:
        return baseURLGlobal
    }
}
</code></pre>
<p>Usage:</p>
<pre><code class="language-go">model := fptcloud.Chat("Qwen3-32B", fptcloud.WithRegion("jp"))
</code></pre>
<p>The JP region hosts <code>Qwen3-32B</code>, <code>Llama-3.3-70B-Instruct</code>, <code>gpt-oss-120b</code>, <code>GLM-4.7</code>, among others. I verified generate and stream against Qwen3-32B. Total file: 132 lines including chat, embeddings, and region routing. <a href="https://goai.sh/providers/fptcloud">FPT Smart Cloud provider docs</a>.</p>
<p>Both providers follow the same shape: <code>resolveOptions</code> reads env vars (<code>CLOUDFLARE_API_TOKEN</code>, <code>FPT_API_KEY</code>, etc.) as fallback, computes the base URL, then <code>Chat()</code> passes a <code>ChatModelConfig</code> to <code>openaicompat.NewChatModel</code>. Only the URL-derivation bit above is unique.</p>
<h2>Compile-time interface checks</h2>
<p>The factory has this block near the top of <code>internal/openaicompat/factory.go</code>:</p>
<pre><code class="language-go">var (
    _ provider.LanguageModel  = (*chatModel)(nil)
    _ provider.CapableModel   = (*chatModel)(nil)
    _ provider.EmbeddingModel = (*embeddingModel)(nil)
)
</code></pre>
<p>It assigns a nil pointer of each concrete type into the interface variable. Renaming an interface method breaks the build immediately, not silently at runtime.</p>
<p>Idiomatic Go, not a GoAI invention. One check covers all 14 providers that route through the factory.</p>
<h2>Testing</h2>
<p>Every provider ships a <code>_test.go</code> using <code>net/http/httptest.NewServer</code> or a custom <code>http.RoundTripper</code> to capture outgoing requests:</p>
<pre><code class="language-go">// Sketch; roundTripperFunc and okResponse are local helpers in the test file.
var gotAuth, gotURL string
tr := roundTripperFunc(func(req *http.Request) (*http.Response, error) {
    gotAuth = req.Header.Get("Authorization")
    gotURL = req.URL.String()
    return okResponse(), nil
})
t.Setenv("CLOUDFLARE_API_TOKEN", "env-tok")
t.Setenv("CLOUDFLARE_ACCOUNT_ID", "env-acc")
m := Chat("m", WithHTTPClient(&amp;http.Client{Transport: tr}))
_, err := m.DoGenerate(t.Context(), params)
// assert gotAuth == "Bearer env-tok", gotURL contains "env-acc"
</code></pre>
<p>No mocking library. The test server (or round-tripper) runs the same code path as production. Streaming tests work the same way, just with Server-Sent Events chunks instead of a JSON body.</p>
<p>All 14 OpenAI-compatible providers reach 100% statement coverage. Factory at 99.8%.</p>
<h2>Functional options</h2>
<p>Every provider exposes the same small set:</p>
<pre><code class="language-go">WithAPIKey(key string)
WithTokenSource(ts provider.TokenSource)
WithBaseURL(url string)
WithHeaders(h map[string]string)
WithHTTPClient(c *http.Client)
</code></pre>
<p>Plus one or two provider-specific ones (<code>WithAccountID</code>, <code>WithRegion</code>). The signature is always <code>func(*options)</code>, so adding a knob doesn't change any constructor.</p>
<p>Not novel, <a href="https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis">Dave Cheney wrote about it in 2014</a>. It's why the 14 providers feel consistent without sharing a base type.</p>
<h2>What Go didn't give me</h2>
<ul>
<li><p><strong>No default arguments.</strong> Every option is a separate <code>With*</code> function. The factory's config struct has 12 fields, most are optional. Zero-value defaults work but grow fragile at 20+ fields.</p>
</li>
<li><p><strong>No decorator pattern.</strong> Telemetry and retry wrap explicitly via hooks, not annotations. Verbose but clear.</p>
</li>
<li><p><strong>No pattern matching.</strong> Response parsing is <code>if/switch</code> on JSON shapes. Rust enums would be cleaner here.</p>
</li>
</ul>
<h2>By the numbers</h2>
<table>
<thead>
<tr>
<th></th>
<th>LOC</th>
</tr>
</thead>
<tbody><tr>
<td>Simple provider (<a href="https://goai.sh/providers/deepinfra">deepinfra</a>, <a href="https://goai.sh/providers/groq">groq</a>, <a href="https://goai.sh/providers/mistral">mistral</a>, ...)</td>
<td>84</td>
</tr>
<tr>
<td>Complex (<a href="https://goai.sh/providers/cloudflare">cloudflare</a>, <a href="https://goai.sh/providers/fptcloud">fptcloud</a> with embeddings)</td>
<td>126-132</td>
</tr>
<tr>
<td>14 OpenAI-compat providers, total</td>
<td>~1,324</td>
</tr>
<tr>
<td>Shared factory in <code>internal/openaicompat</code></td>
<td>334</td>
</tr>
<tr>
<td>Coverage</td>
<td>100% providers, 99.8% factory</td>
</tr>
</tbody></table>
<h2>Takeaway</h2>
<p>The pattern that scales to 14 providers without bloat:</p>
<ul>
<li><p><strong>Split the public surface from the plumbing.</strong> User-facing names (<code>cloudflare.WithAccountID</code>, env var conventions) live in the provider package. HTTP dispatch, token resolution, error parsing live in <code>internal/openaicompat</code>. Changes to the shared code ripple across 14 providers at once without breaking any public API.</p>
</li>
<li><p><strong>Variations as config, not plugins.</strong> Extra body fields, fixed headers, optional auth, account-ID URL building, each is a field on <code>ChatModelConfig</code> or a few lines in <code>resolveOptions</code>. No sub-classing, no registry.</p>
</li>
<li><p><strong>Compile-time checks over documentation.</strong> The <code>var _ LanguageModel = (*chatModel)(nil)</code> assertion at the top of the factory guarantees every provider still satisfies the interface. No runtime surprises.</p>
</li>
</ul>
<p>The factory is 334 lines. Each provider is a few dozen lines of declarations on top.</p>
<p><a href="https://github.com/zendev-sh/goai/releases/tag/v0.7.1">v0.7.1</a> is live. If an inference provider speaks OpenAI-compatible and isn't in GoAI yet, the Cloudflare and FPT diffs are reasonable templates.</p>
<h2>Links</h2>
<ul>
<li><p><a href="https://goai.sh/providers/cloudflare">Cloudflare Workers AI provider</a></p>
</li>
<li><p><a href="https://goai.sh/providers/fptcloud">FPT Smart Cloud provider</a></p>
</li>
<li><p><a href="https://github.com/zendev-sh/goai/releases/tag/v0.7.1">v0.7.1 release</a></p>
</li>
<li><p><a href="https://goai.sh/providers/">Full provider list</a></p>
</li>
<li><p><a href="https://goai.sh/architecture">Architecture</a></p>
</li>
<li><p>Cloudflare request thread: <a href="https://github.com/zendev-sh/goai/issues/44">#44</a> (thanks <a href="https://github.com/adpande">@adpande</a>)</p>
</li>
</ul>
]]></content:encoded></item><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>