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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user