diff --git a/.env.example b/.env.example index 18fa7c8..04455db 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,7 @@ # OPENAI_API_KEY= # ANTHROPIC_API_KEY= # GROQ_API_KEY= +# ABLIT_KEY= # Optional: override config fields via ROOK_ (see configs/rook.example.yaml). # ROOK_AGENT_MODEL=glm-5.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ca8ad..2b03a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to Rook, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Added + +- New built-in provider: `abliteration` - [Abliteration.ai](https://abliteration.ai), an OpenAI-compatible provider hosting less-restrictive models (`abliterated-model`, `abliterated-model-large`) for security research. Export `ABLIT_KEY` and run with `--provider abliteration --model abliterated-model`. Unlike the other built-ins, the engine does not know this provider's endpoint, so the provider seeds its `base_url` (`https://api.abliteration.ai/v1`) from the built-in table. + ## [0.6.2] - unreleased ### Changed diff --git a/configs/rook.example.yaml b/configs/rook.example.yaml index c59fa60..40caf92 100644 --- a/configs/rook.example.yaml +++ b/configs/rook.example.yaml @@ -20,6 +20,7 @@ # together -> TOGETHER_API_KEY cerebras -> CEREBRAS_API_KEY # xai -> XAI_API_KEY moonshot -> MOONSHOT_API_KEY # zai -> ZAI_API_KEY qwen -> DASHSCOPE_API_KEY +# abliteration -> ABLIT_KEY # ollama -> none (local) agent: @@ -88,6 +89,12 @@ providers: anthropic: # api_key: '$ANTHROPIC_API_KEY' + # Abliteration.ai hosts less-restrictive models for security research over the + # OpenAI-compatible API: abliterated-model, abliterated-model-large. + # See https://docs.abliteration.ai. + abliteration: + # api_key: '$ABLIT_KEY' + # A local model needs no key, and never sends data off the machine - the right # default for security work on material that must not leave. ollama: diff --git a/docs/providers.md b/docs/providers.md index 958af2e..33df92c 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -19,6 +19,7 @@ account in between, so all you need is a provider key. Pick a provider with | `xai` | `https://api.x.ai/v1` | `XAI_API_KEY` | | `moonshot` | `https://api.moonshot.cn/v1` | `MOONSHOT_API_KEY` | | `qwen` | DashScope compatible mode | `DASHSCOPE_API_KEY` | +| `abliteration` | `https://api.abliteration.ai/v1` | `ABLIT_KEY` | | `ollama` | `http://localhost:11434/v1` | none (local) | Rook defaults to **`zai`** running **`glm-5.2`** - a strong open model for @@ -49,6 +50,15 @@ right choice - and the one provider that never sends data off-host: rook --provider ollama --model llama-4 "…" ``` +For models that refuse less on offensive-security tasks, +[Abliteration.ai](https://abliteration.ai) hosts less-restrictive variants for +security research over the same OpenAI-compatible API: + +```bash +export ABLIT_KEY="sk-..." +rook --provider abliteration --model abliterated-model "…" +``` + ## Any other provider Anything that speaks the OpenAI-compatible API works. Name a provider, give it a diff --git a/image/desktop/Makefile b/image/desktop/Makefile index bae2008..222c9bf 100644 --- a/image/desktop/Makefile +++ b/image/desktop/Makefile @@ -33,7 +33,7 @@ GPU_DEVICE := $(shell for node in /dev/dri/renderD*; do \ # key can also be saved into the config via `rook config` inside the session. PROVIDER_ENVS := ZAI_API_KEY OPENAI_API_KEY ANTHROPIC_API_KEY GROQ_API_KEY \ MISTRAL_API_KEY DEEPSEEK_API_KEY OPENROUTER_API_KEY TOGETHER_API_KEY \ - CEREBRAS_API_KEY XAI_API_KEY MOONSHOT_API_KEY DASHSCOPE_API_KEY + CEREBRAS_API_KEY XAI_API_KEY MOONSHOT_API_KEY DASHSCOPE_API_KEY ABLIT_KEY KEY_ENV := $(foreach v,$(PROVIDER_ENVS),\ $(if $(strip $($(v))),--env "$(v)=$($(v))",)) diff --git a/internal/config/config.go b/internal/config/config.go index 1819c4d..659c28e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -189,25 +189,29 @@ type ModelConfig struct { // needs. // // The endpoints themselves live in the engine, which is what actually calls -// them; duplicating the URLs here would give two places for them to drift. +// them; duplicating the URLs here would give two places for them to drift. The +// exception is a provider the engine does not know, such as Abliteration.ai - +// its endpoint has to be seeded here or the backend cannot resolve. // Ollama is deliberately included: a local model is the right default for // security work on material that must not leave the machine. var builtinProviders = map[string]struct { secretEnv string // the provider's conventional credential variable + baseURL string // the endpoint, only when the engine does not know the provider }{ - "openai": {secretEnv: "OPENAI_API_KEY"}, - "anthropic": {secretEnv: "ANTHROPIC_API_KEY"}, - "groq": {secretEnv: "GROQ_API_KEY"}, - "mistral": {secretEnv: "MISTRAL_API_KEY"}, - "deepseek": {secretEnv: "DEEPSEEK_API_KEY"}, - "openrouter": {secretEnv: "OPENROUTER_API_KEY"}, - "together": {secretEnv: "TOGETHER_API_KEY"}, - "cerebras": {secretEnv: "CEREBRAS_API_KEY"}, - "xai": {secretEnv: "XAI_API_KEY"}, - "moonshot": {secretEnv: "MOONSHOT_API_KEY"}, - "zai": {secretEnv: "ZAI_API_KEY"}, - "qwen": {secretEnv: "DASHSCOPE_API_KEY"}, - "ollama": {}, + "openai": {secretEnv: "OPENAI_API_KEY"}, + "anthropic": {secretEnv: "ANTHROPIC_API_KEY"}, + "groq": {secretEnv: "GROQ_API_KEY"}, + "mistral": {secretEnv: "MISTRAL_API_KEY"}, + "deepseek": {secretEnv: "DEEPSEEK_API_KEY"}, + "openrouter": {secretEnv: "OPENROUTER_API_KEY"}, + "together": {secretEnv: "TOGETHER_API_KEY"}, + "cerebras": {secretEnv: "CEREBRAS_API_KEY"}, + "xai": {secretEnv: "XAI_API_KEY"}, + "moonshot": {secretEnv: "MOONSHOT_API_KEY"}, + "zai": {secretEnv: "ZAI_API_KEY"}, + "qwen": {secretEnv: "DASHSCOPE_API_KEY"}, + "abliteration": {secretEnv: "ABLIT_KEY", baseURL: "https://api.abliteration.ai/v1"}, + "ollama": {}, } // ProviderDriver returns the driver a provider connection uses, inferring it @@ -275,8 +279,9 @@ func Load(path string) (Config, error) { // credential: a config "$ENV_VAR" reference first, then the provider's // conventional environment variable as a fallback. // -// The endpoint is left empty for a built-in. The engine knows each provider's -// URL, so filling one in here would create a second copy to drift. +// The endpoint is left empty for a built-in the engine knows - the engine fills +// it in, so filling one in here would create a second copy to drift. A built-in +// the engine does not know gets its endpoint seeded from the table above. func resolveProviders(cfg *Config) { if cfg.Providers == nil { cfg.Providers = map[string]ProviderConfig{} @@ -309,6 +314,10 @@ func resolveProviders(cfg *Config) { p.APIKey = strings.TrimSpace(os.Getenv(builtin.secretEnv)) } + if p.BaseURL == "" && isBuiltin && builtin.baseURL != "" { + p.BaseURL = builtin.baseURL + } + for mName, mc := range p.Models { if mc.APIKey != "" { mc.APIKey = resolveSecret(mc.APIKey) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7f956c3..f295ac3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -98,8 +98,9 @@ func TestBuiltinProvidersAreExactlyTheDrivers(t *testing.T) { sort.Strings(seeded) want := []string{ - "anthropic", "cerebras", "deepseek", "groq", "mistral", "moonshot", - "ollama", "openai", "openrouter", "qwen", "together", "xai", "zai", + "abliteration", "anthropic", "cerebras", "deepseek", "groq", "mistral", + "moonshot", "ollama", "openai", "openrouter", "qwen", "together", "xai", + "zai", } if !reflect.DeepEqual(seeded, want) { @@ -180,6 +181,54 @@ func TestOllamaNeedsNoKey(t *testing.T) { } } +// The engine does not know Abliteration.ai, so the built-in is the one +// built-in that carries its endpoint: the provider has to resolve to the +// provider's URL and credential with nothing but ABLIT_KEY exported. +func TestAbliterationSeedsItsEndpoint(t *testing.T) { + isolate(t) + t.Setenv("ABLIT_KEY", "sk-abliteration") + + cfg, err := Load("") + if err != nil { + t.Fatalf("Load: %v", err) + } + + cfg.DefaultProvider = "abliteration" + + selection, err := cfg.Selected() + if err != nil { + t.Fatalf("Selected: %v", err) + } + + if selection.Driver != "abliteration" { + t.Errorf("driver = %q, want abliteration", selection.Driver) + } + + if selection.BaseURL != "https://api.abliteration.ai/v1" { + t.Errorf("base URL = %q, want the provider's endpoint", selection.BaseURL) + } + + if selection.APIKey != "sk-abliteration" { + t.Errorf("key = %q, want the exported one", selection.APIKey) + } + + // an explicit base_url still wins, as it does for every provider + path := writeConfig(t, ` +providers: + abliteration: + base_url: 'https://staging.abliteration.ai/v1' +`) + + cfg, err = Load(path) + if err != nil { + t.Fatalf("Load: %v", err) + } + + if got := cfg.Providers["abliteration"].BaseURL; got != "https://staging.abliteration.ai/v1" { + t.Errorf("base URL = %q, want the configured override", got) + } +} + // A provider connection that names no driver cannot resolve, and says so. func TestAnUnknownProviderIsRejected(t *testing.T) { isolate(t)