mirror of
https://github.com/mudler/LocalAI.git
synced 2026-05-16 20:52:08 -04:00
fix(ollama): accept float-encoded integer options (num_ctx, top_k, ...) Home Assistant's Ollama integration encodes integer options as JSON floats (e.g. `"num_ctx": 8192.0`). Stdlib `json.Unmarshal` refuses to decode a number with fractional notation into an `int` field, so the entire request was rejected with HTTP 400 before reaching the backend: Unmarshal type error: expected=int, got=number 8192.0, field=options.num_ctx Add a custom `UnmarshalJSON` on `OllamaOptions` that routes the int fields (`top_k`, `num_predict`, `seed`, `repeat_last_n`, `num_ctx`) through `*json.Number`, then converts via `Int64()` with a `Float64()` fallback. Public field types are unchanged, so endpoint code is untouched. Float fields and `stop` continue to parse via the default path. Fixes #9837 Assisted-by: Claude Code:claude-opus-4-7 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
352 lines
12 KiB
Go
352 lines
12 KiB
Go
package schema
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// OllamaOptions represents runtime parameters for Ollama generation
|
|
type OllamaOptions struct {
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
TopK *int `json:"top_k,omitempty"`
|
|
NumPredict *int `json:"num_predict,omitempty"`
|
|
RepeatPenalty float64 `json:"repeat_penalty,omitempty"`
|
|
RepeatLastN int `json:"repeat_last_n,omitempty"`
|
|
Seed *int `json:"seed,omitempty"`
|
|
Stop []string `json:"stop,omitempty"`
|
|
NumCtx int `json:"num_ctx,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON accepts integer parameters encoded as either JSON ints
|
|
// (`8192`) or JSON floats (`8192.0`). Some clients - notably Home Assistant's
|
|
// Ollama integration - serialize ints as floats, which stdlib json refuses
|
|
// to decode into int fields. See https://github.com/mudler/LocalAI/issues/9837.
|
|
func (o *OllamaOptions) UnmarshalJSON(data []byte) error {
|
|
type aux struct {
|
|
Temperature *float64 `json:"temperature,omitempty"`
|
|
TopP *float64 `json:"top_p,omitempty"`
|
|
TopK *json.Number `json:"top_k,omitempty"`
|
|
NumPredict *json.Number `json:"num_predict,omitempty"`
|
|
RepeatPenalty float64 `json:"repeat_penalty,omitempty"`
|
|
RepeatLastN *json.Number `json:"repeat_last_n,omitempty"`
|
|
Seed *json.Number `json:"seed,omitempty"`
|
|
Stop []string `json:"stop,omitempty"`
|
|
NumCtx *json.Number `json:"num_ctx,omitempty"`
|
|
}
|
|
var a aux
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return err
|
|
}
|
|
|
|
o.Temperature = a.Temperature
|
|
o.TopP = a.TopP
|
|
o.RepeatPenalty = a.RepeatPenalty
|
|
o.Stop = a.Stop
|
|
|
|
var err error
|
|
if o.TopK, err = jsonNumberToIntPtr(a.TopK); err != nil {
|
|
return fmt.Errorf("options.top_k: %w", err)
|
|
}
|
|
if o.NumPredict, err = jsonNumberToIntPtr(a.NumPredict); err != nil {
|
|
return fmt.Errorf("options.num_predict: %w", err)
|
|
}
|
|
if o.Seed, err = jsonNumberToIntPtr(a.Seed); err != nil {
|
|
return fmt.Errorf("options.seed: %w", err)
|
|
}
|
|
if o.RepeatLastN, err = jsonNumberToInt(a.RepeatLastN); err != nil {
|
|
return fmt.Errorf("options.repeat_last_n: %w", err)
|
|
}
|
|
if o.NumCtx, err = jsonNumberToInt(a.NumCtx); err != nil {
|
|
return fmt.Errorf("options.num_ctx: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jsonNumberToInt parses a json.Number literal as an int, tolerating both
|
|
// integer (`8192`) and float (`8192.0`) encodings. A nil pointer or empty
|
|
// string yields 0, matching the zero-value semantics of the int fields.
|
|
func jsonNumberToInt(n *json.Number) (int, error) {
|
|
if n == nil || *n == "" {
|
|
return 0, nil
|
|
}
|
|
if i, err := n.Int64(); err == nil {
|
|
return int(i), nil
|
|
}
|
|
f, err := n.Float64()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(f), nil
|
|
}
|
|
|
|
func jsonNumberToIntPtr(n *json.Number) (*int, error) {
|
|
if n == nil {
|
|
return nil, nil
|
|
}
|
|
i, err := jsonNumberToInt(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &i, nil
|
|
}
|
|
|
|
// OllamaMessage represents a message in Ollama chat format
|
|
type OllamaMessage struct {
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Images []string `json:"images,omitempty"`
|
|
ToolCalls []any `json:"tool_calls,omitempty"`
|
|
}
|
|
|
|
// OllamaChatRequest represents a request to the Ollama Chat API
|
|
type OllamaChatRequest struct {
|
|
Model string `json:"model"`
|
|
Messages []OllamaMessage `json:"messages"`
|
|
Stream *bool `json:"stream,omitempty"`
|
|
Format any `json:"format,omitempty"`
|
|
Options *OllamaOptions `json:"options,omitempty"`
|
|
Tools []any `json:"tools,omitempty"`
|
|
|
|
// Internal fields
|
|
Context context.Context `json:"-"`
|
|
Cancel context.CancelFunc `json:"-"`
|
|
}
|
|
|
|
// ModelName implements the LocalAIRequest interface
|
|
func (r *OllamaChatRequest) ModelName(s *string) string {
|
|
if s != nil {
|
|
r.Model = *s
|
|
}
|
|
return r.Model
|
|
}
|
|
|
|
// IsStream returns whether streaming is enabled (defaults to true for Ollama)
|
|
func (r *OllamaChatRequest) IsStream() bool {
|
|
if r.Stream == nil {
|
|
return true
|
|
}
|
|
return *r.Stream
|
|
}
|
|
|
|
// OllamaChatResponse represents a response from the Ollama Chat API
|
|
type OllamaChatResponse struct {
|
|
Model string `json:"model"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Message OllamaMessage `json:"message"`
|
|
Done bool `json:"done"`
|
|
DoneReason string `json:"done_reason,omitempty"`
|
|
TotalDuration int64 `json:"total_duration,omitempty"`
|
|
LoadDuration int64 `json:"load_duration,omitempty"`
|
|
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
|
PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"`
|
|
EvalCount int `json:"eval_count,omitempty"`
|
|
EvalDuration int64 `json:"eval_duration,omitempty"`
|
|
}
|
|
|
|
// OllamaGenerateRequest represents a request to the Ollama Generate API
|
|
type OllamaGenerateRequest struct {
|
|
Model string `json:"model"`
|
|
Prompt string `json:"prompt"`
|
|
System string `json:"system,omitempty"`
|
|
Stream *bool `json:"stream,omitempty"`
|
|
Raw bool `json:"raw,omitempty"`
|
|
Format any `json:"format,omitempty"`
|
|
Options *OllamaOptions `json:"options,omitempty"`
|
|
// Context from a previous generate call for continuation
|
|
Context []int `json:"context,omitempty"`
|
|
|
|
// Internal fields
|
|
Ctx context.Context `json:"-"`
|
|
Cancel context.CancelFunc `json:"-"`
|
|
}
|
|
|
|
// ModelName implements the LocalAIRequest interface
|
|
func (r *OllamaGenerateRequest) ModelName(s *string) string {
|
|
if s != nil {
|
|
r.Model = *s
|
|
}
|
|
return r.Model
|
|
}
|
|
|
|
// IsStream returns whether streaming is enabled (defaults to true for Ollama)
|
|
func (r *OllamaGenerateRequest) IsStream() bool {
|
|
if r.Stream == nil {
|
|
return true
|
|
}
|
|
return *r.Stream
|
|
}
|
|
|
|
// OllamaGenerateResponse represents a response from the Ollama Generate API
|
|
type OllamaGenerateResponse struct {
|
|
Model string `json:"model"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Response string `json:"response"`
|
|
Done bool `json:"done"`
|
|
DoneReason string `json:"done_reason,omitempty"`
|
|
Context []int `json:"context,omitempty"`
|
|
TotalDuration int64 `json:"total_duration,omitempty"`
|
|
LoadDuration int64 `json:"load_duration,omitempty"`
|
|
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
|
PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"`
|
|
EvalCount int `json:"eval_count,omitempty"`
|
|
EvalDuration int64 `json:"eval_duration,omitempty"`
|
|
}
|
|
|
|
// OllamaEmbedRequest represents a request to the Ollama Embed API.
|
|
// Ollama's /api/embed endpoint accepts both `input` and `prompt` as the
|
|
// input string value (see https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings),
|
|
// so both keys are deserialized here for client compatibility.
|
|
type OllamaEmbedRequest struct {
|
|
Model string `json:"model"`
|
|
Input any `json:"input,omitempty"` // string or []string
|
|
Prompt any `json:"prompt,omitempty"` // string or []string (Ollama alias for Input)
|
|
Options *OllamaOptions `json:"options,omitempty"`
|
|
}
|
|
|
|
// ModelName implements the LocalAIRequest interface
|
|
func (r *OllamaEmbedRequest) ModelName(s *string) string {
|
|
if s != nil {
|
|
r.Model = *s
|
|
}
|
|
return r.Model
|
|
}
|
|
|
|
// GetInputStrings normalizes the Input/Prompt field to a string slice.
|
|
// Input takes precedence over Prompt when both are provided.
|
|
func (r *OllamaEmbedRequest) GetInputStrings() []string {
|
|
if v := normalizeOllamaEmbedInput(r.Input); v != nil {
|
|
return v
|
|
}
|
|
return normalizeOllamaEmbedInput(r.Prompt)
|
|
}
|
|
|
|
func normalizeOllamaEmbedInput(v any) []string {
|
|
switch v := v.(type) {
|
|
case string:
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
return []string{v}
|
|
case []any:
|
|
var result []string
|
|
for _, item := range v {
|
|
if s, ok := item.(string); ok {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
case []string:
|
|
return v
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OllamaEmbedResponse represents a response from the Ollama Embed API
|
|
type OllamaEmbedResponse struct {
|
|
Model string `json:"model"`
|
|
Embeddings [][]float32 `json:"embeddings"`
|
|
TotalDuration int64 `json:"total_duration,omitempty"`
|
|
LoadDuration int64 `json:"load_duration,omitempty"`
|
|
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
|
}
|
|
|
|
// OllamaShowRequest represents a request to the Ollama Show API
|
|
type OllamaShowRequest struct {
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
Verbose bool `json:"verbose,omitempty"`
|
|
}
|
|
|
|
// ModelName implements the LocalAIRequest interface
|
|
func (r *OllamaShowRequest) ModelName(s *string) string {
|
|
name := r.Name
|
|
if name == "" {
|
|
name = r.Model
|
|
}
|
|
if s != nil {
|
|
r.Name = *s
|
|
}
|
|
return name
|
|
}
|
|
|
|
// OllamaShowResponse represents a response from the Ollama Show API
|
|
type OllamaShowResponse struct {
|
|
Modelfile string `json:"modelfile"`
|
|
Parameters string `json:"parameters"`
|
|
Template string `json:"template"`
|
|
License string `json:"license,omitempty"`
|
|
Details OllamaModelDetails `json:"details"`
|
|
ModelInfo map[string]any `json:"model_info,omitempty"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
}
|
|
|
|
// OllamaModelDetails contains model metadata
|
|
type OllamaModelDetails struct {
|
|
ParentModel string `json:"parent_model,omitempty"`
|
|
Format string `json:"format,omitempty"`
|
|
Family string `json:"family,omitempty"`
|
|
Families []string `json:"families,omitempty"`
|
|
ParameterSize string `json:"parameter_size,omitempty"`
|
|
QuantizationLevel string `json:"quantization_level,omitempty"`
|
|
}
|
|
|
|
// OllamaModelEntry represents a model in the list response
|
|
type OllamaModelEntry struct {
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
ModifiedAt time.Time `json:"modified_at"`
|
|
Size int64 `json:"size"`
|
|
Digest string `json:"digest"`
|
|
Details OllamaModelDetails `json:"details"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
}
|
|
|
|
// OllamaListResponse represents a response from the Ollama Tags API
|
|
type OllamaListResponse struct {
|
|
Models []OllamaModelEntry `json:"models"`
|
|
}
|
|
|
|
// OllamaPsEntry represents a running model in the ps response
|
|
type OllamaPsEntry struct {
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
Size int64 `json:"size"`
|
|
Digest string `json:"digest"`
|
|
Details OllamaModelDetails `json:"details"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
SizeVRAM int64 `json:"size_vram"`
|
|
Capabilities []string `json:"capabilities,omitempty"`
|
|
}
|
|
|
|
// OllamaPsResponse represents a response from the Ollama Ps API
|
|
type OllamaPsResponse struct {
|
|
Models []OllamaPsEntry `json:"models"`
|
|
}
|
|
|
|
// OllamaVersionResponse represents a response from the Ollama Version API
|
|
type OllamaVersionResponse struct {
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
// OllamaPullRequest represents a request to pull a model
|
|
type OllamaPullRequest struct {
|
|
Name string `json:"name"`
|
|
Insecure bool `json:"insecure,omitempty"`
|
|
Stream *bool `json:"stream,omitempty"`
|
|
}
|
|
|
|
// OllamaDeleteRequest represents a request to delete a model
|
|
type OllamaDeleteRequest struct {
|
|
Name string `json:"name"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
// OllamaCopyRequest represents a request to copy a model
|
|
type OllamaCopyRequest struct {
|
|
Source string `json:"source"`
|
|
Destination string `json:"destination"`
|
|
}
|