Introduction
Many developers have enjoyed building tools from scratch: writing web crawlers, implementing instant messaging systems, and building simple databases. This tutorial series follows the same hands-on philosophy, guiding readers to implement an AI Agent CLI tool similar to Claude Code entirely in Go.
The series adopts a unique pedagogical design. Each lesson delivers runnable code stored within an independent folder, and code volumes are intentionally kept compact. The design choice addresses a common pitfall in learning: developers often paste large blocks of code directly into LLMs to understand functionality, which reduces hands-on comprehension. Human cognitive capacity for reading and absorbing source code in a single session is limited. Code length itself forms a learning threshold, and concise code with focused logic delivers higher learning value.
Additional design rules support effective comparison and incremental learning. Code between chapters avoids cross-folder imports, and repeated code segments are preserved intentionally. Learners can compare two different versions side-by-side within a single IDE. Later chapters build incrementally on prior implementations, so developers can observe how logic evolves with simple diff tools. Most importantly, the project relies exclusively on Go standard libraries with zero third-party dependencies. The complete program requires merely four built-in packages, eliminating the burden of researching external library implementations and transitive dependency risks.
The full teaching roadmap spans ten planned episodes, each packaged as a standalone executable module.
| Episode | Folder | Core Topic | Status |
|---|---|---|---|
| 1 | 01-http | Non-stream LLM API calls for three mainstream API dialects | Published |
| 2 | 02-sse | SSE streaming output implementation | Published |
| 3 | 03-cmd | Command-line interactive loop, request wrapping | Completed |
| 4 | 04-console | Full-screen terminal UI, key parsing and vim key bindings | Completed |
| 5 | 05-tool-call | Tool calling implementation, enabling the agent to perform actions beyond chat | Planned |
| 6 | 06-agent-loop | Core Agent main loop | Planned |
| 7 | 07-file-tools | File read and write toolset | Planned |
| 8 | 08-bash | Shell command execution and security sandbox | Planned |
| 9 | 09-permission | Permission control system | Planned |
| 10 | 10-context | Context and token quota management | Planned |
The complete source repository is hosted on GitHub. Developers are strongly recommended to clone the repository locally and follow each lesson sequentially.
Episode 1: Implement Basic LLM HTTP Requests
The first episode covers fundamental LLM API communication. Mainstream LLM API interfaces can be grouped into three primary dialects.
| Dialect | Main adopters | Endpoint | Auth Header | Request Structure |
|---|---|---|---|---|
| Chat Completions | OpenAI compatible APIs, Qwen, Kimi, GLM, Ollama | /v1/chat/completions |
Authorization: Bearer |
messages[], plain string content
|
| Responses | New OpenAI interface | /v1/responses |
Authorization: Bearer |
input array, string or message objects |
| Messages | Anthropic Claude | /v1/messages |
x-api-key + anthropic-version header |
messages[], system as top-level field, mandatory max_tokens
|
The Gemini API uses a separate specification and is excluded from this comparison. All three dialects share the same fundamental workflow, differing only in three dimensions: API endpoint path, authentication header format, and JSON request schema.
The project structure for 01-http separates logic into distinct source files:
-
main.go: Load provider configuration and match target API dialect -
http.go: Shared HTTP transmission layer -
openai_compat.go: Implementation for Chat Completions dialect -
openai_responses.go: Implementation for Responses dialect -
anthropic.go: Implementation for Anthropic Messages dialect
The simplified OpenAI-compatible implementation is shown below. The struct definitions mirror the JSON schema required by Chat Completions endpoints.
type ChatCompletionReq struct {
Model string `json:"model"`
Messages []ChatCompletionMsg `json:"messages"`
Stream bool `json:"stream"`
}
type ChatCompletionMsg struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ChatCompletionResp struct {
Choices []struct {
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
func ChatCompletion(apiKey, baseURL, model, prompt string) (string, error) {
body := ChatCompletionReq{
Model: model,
Messages: []ChatCompletionMsg{
{Role: "system", Content: "You are a concise assistant, keep answers brief."},
{Role: "user", Content: prompt},
},
Stream: false,
}
jsonData, err := json.Marshal(body)
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/chat/completions", bytes.NewReader(jsonData))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 60 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var respBody ChatCompletionResp
err = json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
return "", fmt.Errorf("parse response failed: %w", err)
}
if len(respBody.Choices) == 0 {
return "", fmt.Errorf("response contains no choices")
}
return respBody.Choices[0].Message.Content, nil
}
The core postJSON helper function shared by all three dialects contains roughly ten lines of code. It builds the HTTP request, attaches headers, sends the payload and returns the raw response body.
When running non-stream requests with stream: false, the LLM completes full text generation before returning the entire JSON payload. Developers must configure a reasonable HTTP client timeout value; overly short timeouts terminate requests prematurely.
After running the program and printing the returned JSON payload, developers gain clear visibility of the complete request-response lifecycle of LLM API calls. This completes the first episode implementation.
Episode 2: SSE Streaming Implementation
The second lesson builds upon the basic HTTP client and implements Server-Sent Events (SSE) streaming, the standard protocol for incremental text output in chat applications.
SSE maintains a persistent HTTP connection. The server continuously pushes plain-text data lines over this connection, similar to incremental file downloading. Three core parsing rules govern SSE streams:
- Each line follows
field: valuesyntax. - Empty lines mark the end of an individual event block. Lines starting with
:represent comments and must be skipped. - The
eventanddatafields carry event metadata; application logic only needs to process thedatafield.
Client-side logic keeps the connection alive and reads the response stream line-by-line, parsing each chunk immediately as it arrives.
Differences Between Streaming and Non-streaming Mode
| Item | Non-stream | Streaming |
|---|---|---|
| Request flag | stream: false |
stream: true |
| Response format | Single complete JSON object | Multiple incremental JSON chunks |
| Text rendering | Render after full response received | Render incrementally from delta fragments |
| Timeout handling | Controlled via http.Client.Timeout
|
Cannot enforce static client-side timeout |
The keyword delta refers to incremental fragments. Each chunk only carries newly generated text instead of the full message.
The core streaming implementation for OpenAI-compatible dialects is listed below.
func StreamSSE(apiKey, baseURL, model, prompt string) error {
body := ChatCompletionReq{
Model: model,
Messages: []ChatCompletionMsg{
{Role: "system", Content: "You are a concise assistant."},
{Role: "user", Content: prompt},
},
Stream: true,
}
jsonData, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/chat/completions", bytes.NewReader(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if line == "" || strings.HasPrefix(line, ":") {
continue
}
if !strings.HasPrefix(line, "data: ") {
continue
}
dataPart := strings.TrimPrefix(line, "data: ")
if dataPart == "[DONE]" {
break
}
var chunk struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
} `json:"delta"`
} `json:"choices"`
}
err = json.Unmarshal([]byte(dataPart), &chunk)
if err != nil {
return err
}
if len(chunk.Choices) > 0 {
fmt.Print(chunk.Choices[0].Delta.Content)
}
}
return scanner.Err()
}
The SSE parser reads raw response text line by line. It skips comment lines and empty separators, extracts the data segment and parses embedded JSON objects. A critical detail: streaming requests cannot rely on http.Client.Timeout. The streaming lifecycle is controlled by the server, not client-side timers.
Each API dialect uses different termination markers for streaming sessions:
| Dialect | Delta text field | Termination signal |
|---|---|---|
| Chat Completions | choices[0].delta.content | Plain text [DONE] |
| Responses | output_text within response blocks | response.completed flag |
| Messages (Claude) | delta.text | message_stop event |
The [DONE] marker is plain text and not valid JSON. Code must explicitly intercept this literal string and stop parsing before attempting JSON deserialization.
When developers run the streaming program, text outputs progressively print to the terminal character by character. If incremental text fails to display, it typically indicates the stream flag is missing or intermediate gateway services interrupted SSE transmission.
Subsequent Episodes and Project Roadmap
After implementing the transport layer, the series moves into interactive CLI construction. Episode three creates a command interaction loop that continuously receives user input and wraps requests for the LLM. Episode four builds a full-screen terminal UI with keyboard capture and Vim-style shortcuts.
The core Agent capabilities start from episode five. Tool calling enables the LLM to trigger external functions, moving the program from simple chat to actionable agent workflows. The sixth episode implements the central Agent loop: the core state machine that plans tasks, invokes tools, observes outputs and iterates until objectives finish.
Following episodes implement practical agent tools: file read/write utilities, a constrained bash sandbox for command execution, permission systems for security isolation, and context/token quota management to avoid exceeding model context windows.
Building multi-model agent systems often requires unified routing for different LLM providers. 4sapi serves as an API gateway to consolidate model endpoints, simplifying switching between API dialects during local agent development.
This series intentionally avoids large monolithic codebases. Each lesson remains small and independently runnable. This incremental approach helps developers trace how individual components combine to build a complete agent, rather than copy-pasting finished products. Every component builds sequentially: HTTP transport, streaming parsing, terminal interface, tool calling, agent loop, security controls and context management.
Conclusion
This hands-on Golang tutorial series demystifies the internals of AI Agent CLI applications comparable to Claude Code. Starting from raw HTTP requests and SSE streaming, developers build each layer step by step using only Go standard libraries. The project isolates each feature in independent folders, making code comparison and iterative learning straightforward.
Understanding these low-level API transport and parsing mechanisms is foundational for building custom agents. Developers learn how different LLM API dialects structure payloads, how incremental streaming works, and how to wrap these primitives into interactive command-line agents. The completed foundation can be extended with custom tools, permission guardrails and context management for production-grade AI agent applications.
International access: https://4sapi.com
Domestic access: https://4sapi.cn
Top comments (0)