Adding local LLM support to llm9p is highly feasible and architecturally straightforward. The primary target is GPT-OSS (OpenAI's open-weight models), but the implementation covers any model served via the OpenAI-compatible /v1/chat/completions endpoint. The existing Backend interface already provides the right abstraction. A new OpenAIClient backend (~400-500 lines of Go) would enable llm9p to work with GPT-OSS (via Ollama, vLLM, or llama.cpp), as well as Llama, Mistral, Qwen, and any other model served by these platforms.
GPT-OSS is OpenAI's first open-weight model release since GPT-2, released August 2025 under the Apache 2.0 license. It consists of two Mixture-of-Experts (MoE) models:
| Model | Total Params | Active/Token | VRAM | Context | Target Hardware |
|---|---|---|---|---|---|
| gpt-oss-20b | 21B | 3.6B | ~14-16 GB | 128K | Consumer GPUs (RTX 4090), Apple Silicon |
| gpt-oss-120b | 117B | 5.1B | ~80 GB | 128K | H100/H200/B200 |
/v1/chat/completions -- no special handling needed# Ollama (simplest -- auto-downloads the model)
ollama pull gpt-oss:20b
# API at http://localhost:11434/v1
# vLLM (production, GPU servers)
vllm serve openai/gpt-oss-20b --tool-call-parser openai
# API at http://localhost:8000/v1
# llama.cpp (GGUF quantization, partial GPU offload)
llama-server -hf ggml-org/gpt-oss-20b-GGUF --jinja
# API at http://localhost:8080/v1
# GPT-OSS via Ollama
./llm9p -backend openai -openai-url http://localhost:11434/v1 -model gpt-oss:20b
# GPT-OSS via vLLM
./llm9p -backend openai -openai-url http://localhost:8000/v1 -model openai/gpt-oss-20b
llm9p uses a clean Backend interface (internal/llm/backend.go) with two implementations:
API Backend (Client) |
CLI Backend (CLIClient) |
|
|---|---|---|
| Transport | Anthropic HTTP API | claude subprocess |
| Auth | ANTHROPIC_API_KEY |
Claude Max subscription |
| Token counting | Exact (from API) | Estimated (chars/4) |
| Streaming | SSE via SDK | stdout pipe |
| Tool support | Native tool_use protocol |
Text-only |
The SessionManager wraps any Backend and provides per-session isolation via the stateless AskWithRequest method. This means a new backend only needs to implement the Backend interface -- everything above it (sessions, filesystem, 9P protocol) works unchanged.
Every major local LLM server now exposes an OpenAI-compatible /v1/chat/completions endpoint:
http://localhost:11434/v1/chat/completionsollama run llama3.1:8b (auto-downloads)http://localhost:8080/v1/chat/completions--jinja flag)llama-server -m model.gguf --port 8080/v1/messageshttp://localhost:8000/v1/chat/completions--enable-auto-tool-choice)vllm serve meta-llama/Llama-3.1-8B-Instructhttp://localhost:8080/v1/chat/completionsdocker run -p 8080:8080 localai/localai:latesthttp://localhost:1234/v1/chat/completionsCreate a new OpenAIClient in internal/llm/openai_client.go that implements the Backend interface using the OpenAI Chat Completions API. This is the right abstraction because:
github.com/sashabaranov/go-openai provides a mature, well-maintained Go client with streaming, tool calling, and custom base URL supportSince llama.cpp and LocalAI now support the Anthropic Messages API, we could point the existing Client at a local server using option.WithBaseURL(). However, this is worse because:
// internal/llm/openai_client.go
type OpenAIClient struct {
client *openai.Client
mu sync.RWMutex
model string // e.g., "llama3.1:8b", "mistral"
temperature float64
systemPrompt string
prefill string
messages []Message
lastTokens int
totalTokens int
thinkingTokens int
streaming bool
streamChan chan string
streamDone chan struct{}
}
func NewOpenAIClient(baseURL, apiKey, model string) *OpenAIClient {
config := openai.DefaultConfig(apiKey)
config.BaseURL = baseURL
return &OpenAIClient{
client: openai.NewClientWithConfig(config),
model: model,
temperature: 0.7,
messages: make([]Message, 0),
}
}
The implementation follows the same pattern as the existing CLIClient:
[]MessageCreateChatCompletionStreamAll 18 methods of the Backend interface map cleanly:
| Method | OpenAI Implementation |
|---|---|
Model() / SetModel() |
Local field; model name passed to API |
Temperature() / SetTemperature() |
Local field; sent in request |
SystemPrompt() / SetSystemPrompt() |
Sent as system role message |
ThinkingTokens() / SetThinkingTokens() |
Ignored (local models don't support this) |
Prefill() / SetPrefill() |
Simulated (prepend to response) |
LastTokens() / TotalTokens() |
From API response Usage field |
ContextLimit() |
Configurable per model (default 8K or 32K) |
Compact() |
Use self (local model) for summarization |
Messages() / MessagesJSON() |
Same as existing backends |
AddSystemMessage() / Reset() |
Same as existing backends |
Ask() |
CreateChatCompletion |
AskWithHistory() |
Same with explicit history |
AskWithRequest() |
Full stateless call with tools |
StartStream() / ReadStreamChunk() / IsStreaming() / WaitStream() |
CreateChatCompletionStream |
# Ollama (default port)
./llm9p -backend openai -openai-url http://localhost:11434/v1 -model llama3.1:8b
# llama-server
./llm9p -backend openai -openai-url http://localhost:8080/v1 -model default
# vLLM
./llm9p -backend openai -openai-url http://localhost:8000/v1 -model meta-llama/Llama-3.1-8B-Instruct
# LM Studio
./llm9p -backend openai -openai-url http://localhost:1234/v1 -model local-model
# With API key (for cloud OpenAI-compatible providers)
OPENAI_API_KEY=sk-... ./llm9p -backend openai -openai-url https://api.openai.com/v1 -model gpt-4o
github.com/sashabaranov/go-openai (MIT license, ~8.6k stars)
This library supports:
| Component | Scope |
|---|---|
openai_client.go |
~400-500 lines (following CLIClient patterns) |
main.go changes |
~15 lines (new flag case + validation) |
backend.go |
Add var _ Backend = (*OpenAIClient)(nil) |
| Tests | ~200 lines (unit tests with mock server) |
go.mod |
Add sashabaranov/go-openai dependency |
| Documentation | Update README, CLAUDE.md |
Total: ~700 lines of new code, mostly mechanical since it follows the existing CLIClient structure closely.
ThinkingTokens would be ignored (same as current API backend behavior).| Model | Size | Context | Tool Calling | Notes |
|---|---|---|---|---|
| GPT-OSS 20B | ~14-16 GB | 128K | Yes (strong) | Primary target; MoE, only 3.6B active |
| GPT-OSS 120B | ~80 GB | 128K | Yes (strong) | For GPU servers; beats o4-mini |
| Llama 3.1 8B Instruct | 4-8 GB | 128K | Yes | Best balance of quality and speed |
| Qwen 2.5 7B Instruct | 4-8 GB | 32K | Yes | Strong multilingual, good at tools |
| Mistral 7B Instruct | 4-8 GB | 32K | Yes | Fast, good instruction following |
Adding local LLM support is a well-scoped, low-risk enhancement. The Backend interface is already designed for exactly this kind of extension. GPT-OSS is the primary target -- it offers strong tool calling, 128K context, and runs on consumer hardware. The OpenAI-compatible API is the clear integration point since the entire ecosystem (including GPT-OSS serving via Ollama/vLLM/llama.cpp) has standardized on it. The sashabaranov/go-openai Go library provides everything needed.
The result would make llm9p usable with GPT-OSS and other open-weight models in fully offline/air-gapped environments, eliminate API costs for development and experimentation, and open the door to any model ecosystem.