Ollama: only treat a 404 as model-not-found when the body says so
A 404 from a proxy without /api/chat, or any other 404 error, was
reported as the configured model being missing. Generate now checks the
{"error": ...} payload for Ollama's model-not-found message naming the
configured model (double- or single-quoted); any other 404 stays a
ProviderError with its redacted body excerpt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -138,9 +138,9 @@ func (p *Provider) Generate(ctx context.Context, req llm.Request) (llm.Response,
|
|||||||
raw, err := p.do(ctx, http.MethodPost, "/api/chat", payload)
|
raw, err := p.do(ctx, http.MethodPost, "/api/chat", payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var pe *llm.ProviderError
|
var pe *llm.ProviderError
|
||||||
if errors.As(err, &pe) && pe.Status() == http.StatusNotFound {
|
if errors.As(err, &pe) && pe.Status() == http.StatusNotFound && p.modelNotFound(pe.Body()) {
|
||||||
// Ollama answers 404 when the model is not pulled. The body is
|
// The body is not quoted: the model name is all the caller
|
||||||
// not quoted: the model name is all the caller needs.
|
// needs. Any other 404 keeps its body excerpt below.
|
||||||
return llm.Response{}, fmt.Errorf("ollama: generate: model %q not found: %w", p.model, llm.NewProviderError(pe.Status(), nil, ""))
|
return llm.Response{}, fmt.Errorf("ollama: generate: model %q not found: %w", p.model, llm.NewProviderError(pe.Status(), nil, ""))
|
||||||
}
|
}
|
||||||
return llm.Response{}, fmt.Errorf("ollama: generate: %w", err)
|
return llm.Response{}, fmt.Errorf("ollama: generate: %w", err)
|
||||||
@@ -169,6 +169,21 @@ func (p *Provider) Generate(ctx context.Context, req llm.Request) (llm.Response,
|
|||||||
return resp, fmt.Errorf("ollama: generate: reply is not a JSON object: %w", llm.ErrBadOutput)
|
return resp, fmt.Errorf("ollama: generate: reply is not a JSON object: %w", llm.ErrBadOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modelNotFound reports whether body is Ollama's error for the configured
|
||||||
|
// model not being pulled: {"error": "model \"name\" not found, ..."}, or
|
||||||
|
// with the name in single quotes as newer servers send it. A 404 from
|
||||||
|
// anything else, such as a proxy without /api/chat, is not.
|
||||||
|
func (p *Provider) modelNotFound(body string) bool {
|
||||||
|
var e struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(body), &e); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(e.Error, fmt.Sprintf("model %q not found", p.model)) ||
|
||||||
|
strings.HasPrefix(e.Error, fmt.Sprintf("model '%s' not found", p.model))
|
||||||
|
}
|
||||||
|
|
||||||
// isJSONObject reports whether s is exactly one JSON object.
|
// isJSONObject reports whether s is exactly one JSON object.
|
||||||
func isJSONObject(s string) bool {
|
func isJSONObject(s string) bool {
|
||||||
return strings.HasPrefix(s, "{") && json.Valid([]byte(s))
|
return strings.HasPrefix(s, "{") && json.Valid([]byte(s))
|
||||||
|
|||||||
@@ -327,6 +327,49 @@ func TestGenerateModelMissing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateNotFoundUnrelatedToModel(t *testing.T) {
|
||||||
|
for name, body := range map[string]string{
|
||||||
|
"proxy page": "404 page not found",
|
||||||
|
"other error": `{"error":"no route for /api/chat"}`,
|
||||||
|
"other model": `{"error":"model \"llama3\" not found, try pulling it first"}`,
|
||||||
|
"empty": "",
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
s := newOllamaServer(t, reply{status: 404, body: body})
|
||||||
|
p, err := New(Config{BaseURL: s.URL, Model: "llama9"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Generate(context.Background(), textRequest())
|
||||||
|
var pe *llm.ProviderError
|
||||||
|
if !errors.Is(err, llm.ErrProvider) || !errors.As(err, &pe) || pe.Status() != http.StatusNotFound {
|
||||||
|
t.Fatalf("err = %v, want a ProviderError for HTTP 404", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "not found") && !strings.Contains(body, "not found") {
|
||||||
|
t.Fatalf("Error() = %q claims something was not found", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "llama9") {
|
||||||
|
t.Fatalf("Error() = %q claims the model was not found", err)
|
||||||
|
}
|
||||||
|
if pe.Body() != strings.TrimSpace(body) {
|
||||||
|
t.Fatalf("Body() = %q, want the response body %q", pe.Body(), body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateModelMissingSingleQuoted(t *testing.T) {
|
||||||
|
s := newOllamaServer(t, reply{status: 404, body: `{"error":"model 'llama9' not found"}`})
|
||||||
|
p, err := New(Config{BaseURL: s.URL, Model: "llama9"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = p.Generate(context.Background(), textRequest())
|
||||||
|
if want := `ollama: generate: model "llama9" not found: llm: provider error: HTTP 404`; err == nil || err.Error() != want {
|
||||||
|
t.Fatalf("err = %v, want %q", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestListModels(t *testing.T) {
|
func TestListModels(t *testing.T) {
|
||||||
s := newOllamaServer(t, reply{status: 200, fixture: "tags.json"})
|
s := newOllamaServer(t, reply{status: 200, fixture: "tags.json"})
|
||||||
models, err := newProvider(t, s.URL).ListModels(context.Background())
|
models, err := newProvider(t, s.URL).ListModels(context.Background())
|
||||||
|
|||||||
Reference in New Issue
Block a user