Skip to content

Commit a57e736

Browse files
localai-botmudler
andauthored
fix(ollama): accept prompt alias on /api/embed for Ollama parity (#9780)
Ollama's embedding endpoint accepts both `input` and `prompt` as the input string value (see ollama/ollama docs/api.md#generate-embeddings). LocalAI only accepted `input`, which broke client libraries that send the `prompt` form. Add `Prompt` to OllamaEmbedRequest and have GetInputStrings fall back to it when Input is unset. Input still wins when both are provided. Fixes #9767. Assisted-by: Claude:claude-opus-4-7 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent a689100 commit a57e736

2 files changed

Lines changed: 106 additions & 5 deletions

File tree

core/schema/ollama.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,14 @@ type OllamaGenerateResponse struct {
120120
EvalDuration int64 `json:"eval_duration,omitempty"`
121121
}
122122

123-
// OllamaEmbedRequest represents a request to the Ollama Embed API
123+
// OllamaEmbedRequest represents a request to the Ollama Embed API.
124+
// Ollama's /api/embed endpoint accepts both `input` and `prompt` as the
125+
// input string value (see https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings),
126+
// so both keys are deserialized here for client compatibility.
124127
type OllamaEmbedRequest struct {
125-
Model string `json:"model"`
126-
Input any `json:"input"` // string or []string
128+
Model string `json:"model"`
129+
Input any `json:"input,omitempty"` // string or []string
130+
Prompt any `json:"prompt,omitempty"` // string or []string (Ollama alias for Input)
127131
Options *OllamaOptions `json:"options,omitempty"`
128132
}
129133

@@ -135,10 +139,21 @@ func (r *OllamaEmbedRequest) ModelName(s *string) string {
135139
return r.Model
136140
}
137141

138-
// GetInputStrings normalizes the Input field to a string slice
142+
// GetInputStrings normalizes the Input/Prompt field to a string slice.
143+
// Input takes precedence over Prompt when both are provided.
139144
func (r *OllamaEmbedRequest) GetInputStrings() []string {
140-
switch v := r.Input.(type) {
145+
if v := normalizeOllamaEmbedInput(r.Input); v != nil {
146+
return v
147+
}
148+
return normalizeOllamaEmbedInput(r.Prompt)
149+
}
150+
151+
func normalizeOllamaEmbedInput(v any) []string {
152+
switch v := v.(type) {
141153
case string:
154+
if v == "" {
155+
return nil
156+
}
142157
return []string{v}
143158
case []any:
144159
var result []string

core/schema/ollama_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package schema_test
2+
3+
import (
4+
"encoding/json"
5+
6+
. "github.com/mudler/LocalAI/core/schema"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
)
11+
12+
var _ = Describe("OllamaEmbedRequest", func() {
13+
14+
Context("GetInputStrings", func() {
15+
It("returns a single string when Input is a string", func() {
16+
req := OllamaEmbedRequest{Input: "hello world"}
17+
18+
Expect(req.GetInputStrings()).To(Equal([]string{"hello world"}))
19+
})
20+
21+
It("returns a list of strings when Input is a []string", func() {
22+
req := OllamaEmbedRequest{Input: []string{"hello", "world"}}
23+
24+
Expect(req.GetInputStrings()).To(Equal([]string{"hello", "world"}))
25+
})
26+
27+
It("returns a list of strings when Input is a []any (post JSON unmarshal)", func() {
28+
req := OllamaEmbedRequest{Input: []any{"hello", "world"}}
29+
30+
Expect(req.GetInputStrings()).To(Equal([]string{"hello", "world"}))
31+
})
32+
})
33+
34+
Context("JSON unmarshaling (Ollama API compatibility)", func() {
35+
It("accepts the 'input' field as a single string", func() {
36+
body := []byte(`{"model": "m", "input": "why is the sky blue?"}`)
37+
38+
var req OllamaEmbedRequest
39+
Expect(json.Unmarshal(body, &req)).To(Succeed())
40+
41+
Expect(req.Model).To(Equal("m"))
42+
Expect(req.GetInputStrings()).To(Equal([]string{"why is the sky blue?"}))
43+
})
44+
45+
It("accepts the 'input' field as an array of strings", func() {
46+
body := []byte(`{"model": "m", "input": ["why is the sky blue?", "why is the grass green?"]}`)
47+
48+
var req OllamaEmbedRequest
49+
Expect(json.Unmarshal(body, &req)).To(Succeed())
50+
51+
Expect(req.GetInputStrings()).To(Equal([]string{"why is the sky blue?", "why is the grass green?"}))
52+
})
53+
54+
// Ollama's embedding endpoint accepts both `input` and `prompt` keys:
55+
// https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings
56+
// LocalAI must accept `prompt` so client libraries using that key are not broken.
57+
// See https://github.com/mudler/LocalAI/issues/9767.
58+
It("accepts the 'prompt' field as a single string (Ollama compatibility)", func() {
59+
body := []byte(`{"model": "m", "prompt": "why is the sky blue?"}`)
60+
61+
var req OllamaEmbedRequest
62+
Expect(json.Unmarshal(body, &req)).To(Succeed())
63+
64+
Expect(req.Model).To(Equal("m"))
65+
Expect(req.GetInputStrings()).To(Equal([]string{"why is the sky blue?"}))
66+
})
67+
68+
It("accepts the 'prompt' field as an array of strings (Ollama compatibility)", func() {
69+
body := []byte(`{"model": "m", "prompt": ["why is the sky blue?", "why is the grass green?"]}`)
70+
71+
var req OllamaEmbedRequest
72+
Expect(json.Unmarshal(body, &req)).To(Succeed())
73+
74+
Expect(req.GetInputStrings()).To(Equal([]string{"why is the sky blue?", "why is the grass green?"}))
75+
})
76+
77+
It("prefers 'input' when both 'input' and 'prompt' are provided", func() {
78+
body := []byte(`{"model": "m", "input": "from input", "prompt": "from prompt"}`)
79+
80+
var req OllamaEmbedRequest
81+
Expect(json.Unmarshal(body, &req)).To(Succeed())
82+
83+
Expect(req.GetInputStrings()).To(Equal([]string{"from input"}))
84+
})
85+
})
86+
})

0 commit comments

Comments
 (0)