diff --git a/README.md b/README.md index 2658ab3..240efd2 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ majordomo failover chain / alias) is used verbatim. |----------|-------------------|---------|--------| | **Ollama Cloud** (default) | `ollama-cloud` | `OLLAMA_API_KEY` / `OLLAMA_CLOUD_API_KEY` | ✅ in active use | | **Local Ollama** | `ollama` | none (`OLLAMA_HOST` or `GADFLY_BASE_URL` for a remote daemon) | ✅ tested | +| **[foreman](https://gitea.stevedudenhoeffer.com/steve/foreman)** (native-Ollama queue daemon) | `foreman` + `GADFLY_BASE_URL`, or a `GADFLY_ENDPOINT_*` / `LLM_*` `foreman://` entry | optional bearer (via the endpoint/DSN) | ✅ native-Ollama path | | **OpenAI-compatible** (incl. local Ollama's `/v1`) | `openai` + `GADFLY_BASE_URL` | `OPENAI_API_KEY` (any non-empty for Ollama) | ✅ tested against Ollama | | **OpenAI** | `openai` | `OPENAI_API_KEY` | ⚠️ wired, **untested** | | **Anthropic** | `anthropic` | `ANTHROPIC_API_KEY` | ⚠️ wired, **untested** | @@ -87,7 +88,8 @@ name with env vars and then reference `name/model` in `GADFLY_MODEL`/`GADFLY_MOD # http-capable (Gadfly-native) — base URL used verbatim, so plaintext LAN works: GADFLY_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434" GADFLY_ENDPOINT_GPU="openai|http://gpu.lan:8000/v1|sk-local" -GADFLY_MODELS="bigbox/qwen2.5-coder:7b,gpu/llama3.1" +GADFLY_ENDPOINT_M1="foreman|http://foreman-m1:8080|tok" # native-Ollama queue daemon +GADFLY_MODELS="bigbox/qwen2.5-coder:7b,gpu/llama3.1,m1/qwen3:14b" # pure spec alias (a model, or a failover chain): GADFLY_ALIAS_FAST="bigbox/qwen2.5-coder:7b,ollama-cloud/gpt-oss:120b-cloud" @@ -95,9 +97,9 @@ GADFLY_MODEL="fast" ``` `` is lowercased to form the registry name (`GADFLY_ENDPOINT_BIGBOX` → `bigbox`). This -is the same idea as majordomo's built-in **`LLM_*` env DSNs** (`LLM_BIGBOX=ollama://tok@host`), -which Gadfly also honors — but those are **HTTPS-only**, so for plaintext local Ollama use -`GADFLY_ENDPOINT_*` instead. +is the same idea as majordomo's built-in **`LLM_*` env DSNs** (`LLM_BIGBOX=ollama://tok@host`, +`LLM_M1=foreman://tok@host`), which Gadfly also honors — but those are **HTTPS-only**, so for a +plaintext local Ollama or `http://` foreman use `GADFLY_ENDPOINT_*` instead. > **Gitea Actions note:** repo `vars`/`secrets` aren't auto-exposed as env — add each alias to > the stub workflow's `env:` block, e.g. `GADFLY_ENDPOINT_BIGBOX: ${{ vars.GADFLY_ENDPOINT_BIGBOX }}`. diff --git a/cmd/gadfly/model.go b/cmd/gadfly/model.go index 097baf1..03d0a4e 100644 --- a/cmd/gadfly/model.go +++ b/cmd/gadfly/model.go @@ -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_ = "|[|]" // 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_ = "" // 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) } } diff --git a/cmd/gadfly/model_test.go b/cmd/gadfly/model_test.go index 3475120..559c9c5 100644 --- a/cmd/gadfly/model_test.go +++ b/cmd/gadfly/model_test.go @@ -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 { diff --git a/examples/endpoint-aliases.yml b/examples/endpoint-aliases.yml index a1eba46..64ce0e0 100644 --- a/examples/endpoint-aliases.yml +++ b/examples/endpoint-aliases.yml @@ -5,10 +5,15 @@ # can then reference as "/" (NAME lowercases: BIGBOX -> bigbox). # The base URL is used verbatim, so plaintext http LAN endpoints work. # +# provider is ollama / foreman / openai / anthropic / google. "foreman" targets a +# foreman queue daemon (https://gitea.stevedudenhoeffer.com/steve/foreman) — native +# Ollama on the wire, so just give it the daemon's URL (and optional bearer token). +# # Gitea note: vars/secrets aren't auto-exposed as env, so map each alias here. -# Suggested repo vars: +# Suggested repo vars (and a secret when the value carries a token): # GADFLY_ENDPOINT_BIGBOX = ollama|http://192.168.1.50:11434 # GADFLY_ENDPOINT_GPU = openai|http://gpu.lan:8000/v1 +# GADFLY_ENDPOINT_M1 = foreman|http://foreman-m1:8080| (use a secret) name: Adversarial Review (Gadfly) @@ -49,8 +54,9 @@ jobs: # --- named endpoints (mapped from repo vars) --- GADFLY_ENDPOINT_BIGBOX: ${{ vars.GADFLY_ENDPOINT_BIGBOX }} # "ollama|http://192.168.1.50:11434" GADFLY_ENDPOINT_GPU: ${{ vars.GADFLY_ENDPOINT_GPU }} # "openai|http://gpu.lan:8000/v1" + GADFLY_ENDPOINT_M1: ${{ secrets.GADFLY_ENDPOINT_M1 }} # "foreman|http://foreman-m1:8080|" # one reviewer (one comment) per model, across the aliased endpoints: - GADFLY_MODELS: "bigbox/qwen2.5-coder:7b,gpu/llama3.1" + GADFLY_MODELS: "bigbox/qwen2.5-coder:7b,gpu/llama3.1,m1/qwen3:14b" # --- event context (leave as-is) --- EVENT_NAME: ${{ github.event_name }} PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}