Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# OPENAI_API_KEY=
# ANTHROPIC_API_KEY=
# GROQ_API_KEY=
# ABLIT_KEY=

# Optional: override config fields via ROOK_<PATH> (see configs/rook.example.yaml).
# ROOK_AGENT_MODEL=glm-5.2
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions configs/rook.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion image/desktop/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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))",))

Expand Down
41 changes: 25 additions & 16 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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)
Expand Down
53 changes: 51 additions & 2 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down