feat(v2/ollama): scaffold native /api/chat provider

Adds wire types and a Provider struct that will replace the
openaicompat-based Ollama shim with a native /api/chat implementation.
Complete and Stream methods are stubs; subsequent commits implement them.

Adjusts the existing ollama.go to drop the type alias on
openaicompat.Provider (renaming the legacy shim to a temporary internal
helper) so the new native Provider type does not collide. Public New()
still returns the openaicompat-backed provider until Task 4 swaps it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 18:22:11 +00:00
parent 5c5d861915
commit 0e358148eb
2 changed files with 151 additions and 7 deletions
+133
View File
@@ -0,0 +1,133 @@
// 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
}
+18 -7
View File
@@ -9,17 +9,28 @@ import (
"gitea.stevedudenhoeffer.com/steve/go-llm/v2/openaicompat"
)
// DefaultBaseURL points at a local Ollama instance with default port.
// DefaultBaseURL points at a local Ollama instance with default port. Kept
// for the openaicompat-based shim; callers should migrate to
// DefaultLocalBaseURL (no /v1 suffix) which targets the native /api/chat
// endpoint.
const DefaultBaseURL = "http://localhost:11434/v1"
// Provider is a type alias over openaicompat.Provider.
type Provider = openaicompat.Provider
// New creates a new Ollama provider. An empty baseURL uses DefaultBaseURL.
// Ollama ignores the API key; callers may pass "".
func New(apiKey, baseURL string) *Provider {
// shimNew is the legacy openaicompat-based constructor, retained until the
// native provider's Complete/Stream are fully implemented (Task 4 replaces
// the public New() with a native-backed constructor).
func shimNew(apiKey, baseURL string) *openaicompat.Provider {
if baseURL == "" {
baseURL = DefaultBaseURL
}
return openaicompat.New(apiKey, baseURL, openaicompat.Rules{})
}
// New creates a new Ollama provider. An empty baseURL uses DefaultBaseURL.
// Ollama ignores the API key; callers may pass "".
//
// Note: this constructor currently still routes through the openaicompat
// shim. Subsequent commits replace the body with a native /api/chat
// implementation backed by the Provider type in native.go.
func New(apiKey, baseURL string) *openaicompat.Provider {
return shimNew(apiKey, baseURL)
}