// Package ollama implements the go-llm v2 provider interface for Ollama, // targeting Ollama's native /api/chat endpoint. Supports both local Ollama // instances (no API key) and Ollama Cloud (https://ollama.com, requires an // API key). package ollama import ( "context" "encoding/json" "errors" "net/http" "gitea.stevedudenhoeffer.com/steve/go-llm/v2/provider" ) // DefaultLocalBaseURL is the default base URL for a locally-running Ollama // instance. const DefaultLocalBaseURL = "http://localhost:11434" // DefaultCloudBaseURL is the default base URL for Ollama Cloud. const DefaultCloudBaseURL = "https://ollama.com" // Provider implements provider.Provider over Ollama's native /api/chat // endpoint. An empty apiKey means local-mode (no Authorization header sent); // a non-empty apiKey is sent as a Bearer token (cloud-mode). type Provider struct { apiKey string baseURL string client *http.Client } // newNative constructs a native Ollama provider. Callers should use the // package-level New() constructor or the v2 llm.Ollama() / llm.OllamaCloud() // helpers. func newNative(apiKey, baseURL string) *Provider { return &Provider{ apiKey: apiKey, baseURL: baseURL, client: &http.Client{}, } } // nativeChatRequest is the JSON body POSTed to /api/chat. type nativeChatRequest struct { Model string `json:"model"` Messages []nativeChatMessage `json:"messages"` Tools []nativeToolDef `json:"tools,omitempty"` Stream bool `json:"stream"` // Think is polymorphic — Ollama accepts true/false or "low"/"medium"/"high". Think json.RawMessage `json:"think,omitempty"` Options map[string]any `json:"options,omitempty"` } // nativeChatMessage is one entry in the messages array on the wire. It also // carries assistant tool calls and tool-role responses. type nativeChatMessage struct { Role string `json:"role"` Content string `json:"content,omitempty"` Images []string `json:"images,omitempty"` ToolCalls []nativeToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` Thinking string `json:"thinking,omitempty"` } // nativeToolCall mirrors Ollama's tool-call wire shape: a function with name // and JSON-encoded arguments. Ollama's spec doesn't require an id, but some // builds and some streaming chunks include one — we accept it on both wire and // internal sides. type nativeToolCall struct { ID string `json:"id,omitempty"` Function nativeFunctionCall `json:"function"` } type nativeFunctionCall struct { Index *int `json:"index,omitempty"` Name string `json:"name,omitempty"` Arguments json.RawMessage `json:"arguments,omitempty"` } // nativeChatResponse is the JSON body returned from a non-streaming /api/chat // call (and is also the per-line shape during streaming). type nativeChatResponse struct { Model string `json:"model,omitempty"` Message nativeChatMessage `json:"message"` Done bool `json:"done"` DoneReason string `json:"done_reason,omitempty"` PromptEvalCount int `json:"prompt_eval_count,omitempty"` EvalCount int `json:"eval_count,omitempty"` TotalDuration int64 `json:"total_duration,omitempty"` } // nativeToolDef is the wire shape of a tool definition sent to Ollama. type nativeToolDef struct { Type string `json:"type"` Function nativeFunctionDef `json:"function"` } type nativeFunctionDef struct { Name string `json:"name"` Description string `json:"description,omitempty"` Parameters map[string]any `json:"parameters,omitempty"` } // encodeThink converts a go-llm Reasoning string ("", "low", "medium", // "high", or the literal strings "true"/"false") into Ollama's polymorphic // `think` field. Returns nil for the empty string so the field is omitted. func encodeThink(reasoning string) json.RawMessage { switch reasoning { case "": return nil case "true": return json.RawMessage(`true`) case "false": return json.RawMessage(`false`) default: // "low" / "medium" / "high" — encode as a JSON string. b, _ := json.Marshal(reasoning) return b } } var errNotImplemented = errors.New("ollama native provider: not implemented") // Complete performs a non-streaming chat completion via /api/chat. func (p *Provider) Complete(ctx context.Context, req provider.Request) (provider.Response, error) { return provider.Response{}, errNotImplemented } // Stream performs a streaming chat completion via /api/chat with // `stream: true`, parsing NDJSON line-by-line. func (p *Provider) Stream(ctx context.Context, req provider.Request, events chan<- provider.StreamEvent) error { return errNotImplemented }