0e358148eb
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>
134 lines
4.7 KiB
Go
134 lines
4.7 KiB
Go
// 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
|
|
}
|