feat: add foreman provider type for endpoint overrides
Build & push image / build-and-push (push) Successful in 7s
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:
@@ -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 |
|
| **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 |
|
| **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-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** |
|
| **OpenAI** | `openai` | `OPENAI_API_KEY` | ⚠️ wired, **untested** |
|
||||||
| **Anthropic** | `anthropic` | `ANTHROPIC_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:
|
# http-capable (Gadfly-native) — base URL used verbatim, so plaintext LAN works:
|
||||||
GADFLY_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434"
|
GADFLY_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434"
|
||||||
GADFLY_ENDPOINT_GPU="openai|http://gpu.lan:8000/v1|sk-local"
|
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):
|
# pure spec alias (a model, or a failover chain):
|
||||||
GADFLY_ALIAS_FAST="bigbox/qwen2.5-coder:7b,ollama-cloud/gpt-oss:120b-cloud"
|
GADFLY_ALIAS_FAST="bigbox/qwen2.5-coder:7b,ollama-cloud/gpt-oss:120b-cloud"
|
||||||
@@ -95,9 +97,9 @@ GADFLY_MODEL="fast"
|
|||||||
```
|
```
|
||||||
|
|
||||||
`<NAME>` is lowercased to form the registry name (`GADFLY_ENDPOINT_BIGBOX` → `bigbox`). This
|
`<NAME>` 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`),
|
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
|
`LLM_M1=foreman://tok@host`), which Gadfly also honors — but those are **HTTPS-only**, so for a
|
||||||
`GADFLY_ENDPOINT_*` instead.
|
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
|
> **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 }}`.
|
> the stub workflow's `env:` block, e.g. `GADFLY_ENDPOINT_BIGBOX: ${{ vars.GADFLY_ENDPOINT_BIGBOX }}`.
|
||||||
|
|||||||
+16
-3
@@ -79,6 +79,11 @@ func resolveModel() (llm.Model, error) {
|
|||||||
opts = append(opts, ollama.WithToken(apiKey))
|
opts = append(opts, ollama.WithToken(apiKey))
|
||||||
}
|
}
|
||||||
return ollama.New(opts...).Model(model)
|
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":
|
case "anthropic":
|
||||||
opts := []anthropic.Option{anthropic.WithBaseURL(baseURL)}
|
opts := []anthropic.Option{anthropic.WithBaseURL(baseURL)}
|
||||||
if apiKey != "" {
|
if apiKey != "" {
|
||||||
@@ -92,7 +97,7 @@ func resolveModel() (llm.Model, error) {
|
|||||||
}
|
}
|
||||||
return google.New(opts...).Model(model)
|
return google.New(opts...).Model(model)
|
||||||
default:
|
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>]"
|
// GADFLY_ENDPOINT_<NAME> = "<provider>|<base-url>[|<key>]"
|
||||||
// Registers a provider at an explicit endpoint. Unlike majordomo's LLM_*
|
// Registers a provider at an explicit endpoint. Unlike majordomo's LLM_*
|
||||||
// DSNs (which are HTTPS-only), the base URL is used verbatim, so a
|
// 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_ENDPOINT_BIGBOX="ollama|http://192.168.1.50:11434"
|
||||||
// GADFLY_MODEL=bigbox/qwen2.5-coder:7b
|
// 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>"
|
// GADFLY_ALIAS_<NAME> = "<majordomo spec>"
|
||||||
// Registers a plain alias that expands inline (a model, or a failover
|
// 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))
|
opts = append(opts, ollama.WithToken(key))
|
||||||
}
|
}
|
||||||
return ollama.New(opts...), nil
|
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":
|
case "openai", "openai-compatible":
|
||||||
opts := []openai.Option{openai.WithName(name), openai.WithBaseURL(baseURL)}
|
opts := []openai.Option{openai.WithName(name), openai.WithBaseURL(baseURL)}
|
||||||
if key != "" {
|
if key != "" {
|
||||||
@@ -209,6 +222,6 @@ func endpointProvider(name, raw string) (llm.Provider, error) {
|
|||||||
}
|
}
|
||||||
return google.New(opts...), nil
|
return google.New(opts...), nil
|
||||||
default:
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ func TestEndpointProvider(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
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"} {
|
for _, bad := range []string{"", "ollama", "noprovider-no-pipe", "mystery|http://x"} {
|
||||||
t.Run("rejects "+bad, func(t *testing.T) {
|
t.Run("rejects "+bad, func(t *testing.T) {
|
||||||
if _, err := endpointProvider("n", bad); err == nil {
|
if _, err := endpointProvider("n", bad); err == nil {
|
||||||
|
|||||||
@@ -5,10 +5,15 @@
|
|||||||
# can then reference as "<name>/<model>" (NAME lowercases: BIGBOX -> bigbox).
|
# can then reference as "<name>/<model>" (NAME lowercases: BIGBOX -> bigbox).
|
||||||
# The base URL is used verbatim, so plaintext http LAN endpoints work.
|
# 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.
|
# 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_BIGBOX = ollama|http://192.168.1.50:11434
|
||||||
# GADFLY_ENDPOINT_GPU = openai|http://gpu.lan:8000/v1
|
# GADFLY_ENDPOINT_GPU = openai|http://gpu.lan:8000/v1
|
||||||
|
# GADFLY_ENDPOINT_M1 = foreman|http://foreman-m1:8080|<token> (use a secret)
|
||||||
|
|
||||||
name: Adversarial Review (Gadfly)
|
name: Adversarial Review (Gadfly)
|
||||||
|
|
||||||
@@ -49,8 +54,9 @@ jobs:
|
|||||||
# --- named endpoints (mapped from repo vars) ---
|
# --- named endpoints (mapped from repo vars) ---
|
||||||
GADFLY_ENDPOINT_BIGBOX: ${{ vars.GADFLY_ENDPOINT_BIGBOX }} # "ollama|http://192.168.1.50:11434"
|
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_GPU: ${{ vars.GADFLY_ENDPOINT_GPU }} # "openai|http://gpu.lan:8000/v1"
|
||||||
|
GADFLY_ENDPOINT_M1: ${{ secrets.GADFLY_ENDPOINT_M1 }} # "foreman|http://foreman-m1:8080|<token>"
|
||||||
# one reviewer (one comment) per model, across the aliased endpoints:
|
# 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 context (leave as-is) ---
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
EVENT_NAME: ${{ github.event_name }}
|
||||||
PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
PR: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number }}
|
||||||
|
|||||||
Reference in New Issue
Block a user