feat: add foreman provider type for endpoint overrides
Build & push image / build-and-push (push) Successful in 7s

Accept "foreman" in both resolveModel (GADFLY_BASE_URL) and endpointProvider
(GADFLY_ENDPOINT_*) switches, mapping to majordomo's ollama.Foreman() preset
(handles foreman's non-streaming/long-poll quirks). Unlike the HTTPS-only
LLM_* foreman:// DSN, the base URL is verbatim, so a plaintext http:// foreman
queue works. Tests + README provider table + endpoint-aliases example updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 20:13:47 -04:00
parent a1e9d109e5
commit 6e3a83c437
4 changed files with 45 additions and 9 deletions
+16 -3
View File
@@ -79,6 +79,11 @@ func resolveModel() (llm.Model, error) {
opts = append(opts, ollama.WithToken(apiKey))
}
return ollama.New(opts...).Model(model)
case "foreman":
// foreman (gitea.stevedudenhoeffer.com/steve/foreman) is a native-Ollama
// queue daemon; the preset also smooths its non-streaming/long-poll
// quirks. Base URL is used verbatim, so a plaintext http:// foreman works.
return ollama.Foreman(baseURL, apiKey).Model(model)
case "anthropic":
opts := []anthropic.Option{anthropic.WithBaseURL(baseURL)}
if apiKey != "" {
@@ -92,7 +97,7 @@ func resolveModel() (llm.Model, error) {
}
return google.New(opts...).Model(model)
default:
return nil, fmt.Errorf("GADFLY_BASE_URL is set but GADFLY_PROVIDER %q has no endpoint-override support (use openai/ollama/anthropic/google, or unset GADFLY_BASE_URL to resolve via majordomo)", provider)
return nil, fmt.Errorf("GADFLY_BASE_URL is set but GADFLY_PROVIDER %q has no endpoint-override support (use openai/ollama/foreman/anthropic/google, or unset GADFLY_BASE_URL to resolve via majordomo)", provider)
}
}
@@ -131,9 +136,12 @@ func buildSpec(provider, model string) string {
// GADFLY_ENDPOINT_<NAME> = "<provider>|<base-url>[|<key>]"
// Registers a provider at an explicit endpoint. Unlike majordomo's LLM_*
// DSNs (which are HTTPS-only), the base URL is used verbatim, so a
// plaintext local Ollama works:
// plaintext local Ollama (or foreman queue) works:
// GADFLY_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434"
// GADFLY_MODEL=bigbox/qwen2.5-coder:7b
// provider is one of ollama/foreman/openai/anthropic/google; "foreman"
// targets a foreman daemon (native Ollama on the wire):
// GADFLY_ENDPOINT_M1="foreman|http://foreman-m1:8080|tok"
//
// GADFLY_ALIAS_<NAME> = "<majordomo spec>"
// Registers a plain alias that expands inline (a model, or a failover
@@ -190,6 +198,11 @@ func endpointProvider(name, raw string) (llm.Provider, error) {
opts = append(opts, ollama.WithToken(key))
}
return ollama.New(opts...), nil
case "foreman":
// foreman is native-Ollama on the wire; the preset additionally handles
// its non-streaming degradation. Unlike the HTTPS-only LLM_* foreman://
// DSN, the base URL here is verbatim, so a plaintext http:// foreman works.
return ollama.Foreman(baseURL, key, ollama.WithName(name)), nil
case "openai", "openai-compatible":
opts := []openai.Option{openai.WithName(name), openai.WithBaseURL(baseURL)}
if key != "" {
@@ -209,6 +222,6 @@ func endpointProvider(name, raw string) (llm.Provider, error) {
}
return google.New(opts...), nil
default:
return nil, fmt.Errorf("unknown provider %q (use ollama/openai/anthropic/google)", provider)
return nil, fmt.Errorf("unknown provider %q (use ollama/foreman/openai/anthropic/google)", provider)
}
}
+15
View File
@@ -17,6 +17,21 @@ func TestEndpointProvider(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("foreman queue registers under its name", func(t *testing.T) {
p, err := endpointProvider("m1", "foreman|http://foreman-m1:8080|tok")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// WithName(name) must win over the Foreman preset's default "foreman".
if p.Name() != "m1" {
t.Errorf("Name() = %q, want %q", p.Name(), "m1")
}
})
t.Run("foreman without token", func(t *testing.T) {
if _, err := endpointProvider("m5", "foreman|http://foreman-m5:8080"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
for _, bad := range []string{"", "ollama", "noprovider-no-pipe", "mystery|http://x"} {
t.Run("rejects "+bad, func(t *testing.T) {
if _, err := endpointProvider("n", bad); err == nil {