Skip to content
Merged
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
76 changes: 76 additions & 0 deletions alembic/versions/005_v05_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""v0.5 users table and user_id foreign keys.

Revision ID: 005
Revises: 004
Create Date: 2026-02-17
"""

from __future__ import annotations

import sqlalchemy as sa
from alembic import op

revision: str = "005"
down_revision: str = "004"
branch_labels: tuple[str, ...] | None = None
depends_on: str | None = None


def upgrade() -> None:
op.create_table(
"users",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("email", sa.String(255), nullable=False, unique=True),
sa.Column("password_hash", sa.String(128), nullable=False),
sa.Column("display_name", sa.String(100), nullable=False),
sa.Column("role", sa.String(20), nullable=False, server_default="contributor"),
sa.Column(
"is_active", sa.Boolean(), nullable=False, server_default=sa.text("1")
),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
)
op.create_index("ix_users_email", "users", ["email"], unique=True)

# Add user_id column to threads (batch mode for SQLite compatibility)
with op.batch_alter_table("threads") as batch_op:
batch_op.add_column(sa.Column("user_id", sa.String(36), nullable=True))
batch_op.create_index("ix_threads_user_id", ["user_id"])
batch_op.create_foreign_key(
"fk_threads_user_id", "users", ["user_id"], ["id"]
)

# Add user_id column to decisions
with op.batch_alter_table("decisions") as batch_op:
batch_op.add_column(sa.Column("user_id", sa.String(36), nullable=True))
batch_op.create_index("ix_decisions_user_id", ["user_id"])
batch_op.create_foreign_key(
"fk_decisions_user_id", "users", ["user_id"], ["id"]
)

# Add user_id column to api_keys
with op.batch_alter_table("api_keys") as batch_op:
batch_op.add_column(sa.Column("user_id", sa.String(36), nullable=True))
batch_op.create_index("ix_api_keys_user_id", ["user_id"])
batch_op.create_foreign_key(
"fk_api_keys_user_id", "users", ["user_id"], ["id"]
)


def downgrade() -> None:
with op.batch_alter_table("api_keys") as batch_op:
batch_op.drop_index("ix_api_keys_user_id")
batch_op.drop_constraint("fk_api_keys_user_id", type_="foreignkey")
batch_op.drop_column("user_id")

with op.batch_alter_table("decisions") as batch_op:
batch_op.drop_index("ix_decisions_user_id")
batch_op.drop_constraint("fk_decisions_user_id", type_="foreignkey")
batch_op.drop_column("user_id")

with op.batch_alter_table("threads") as batch_op:
batch_op.drop_index("ix_threads_user_id")
batch_op.drop_constraint("fk_threads_user_id", type_="foreignkey")
batch_op.drop_column("user_id")

op.drop_table("users")
5 changes: 5 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ Run a consensus query.
| `rounds` | integer | `3` | Max consensus rounds |
| `decompose` | boolean | `false` | Decompose into subtasks first |
| `tools` | boolean | `false` | Enable tool use |
| `panel` | list[string] | `null` | Restrict to these model refs only (e.g. `["anthropic:claude-opus-4-6", "openai:gpt-5.2"]`) |
| `proposer` | string | `null` | Override the proposer model ref |
| `challengers` | list[string] | `null` | Override the challenger model refs |

**Response (200):**

Expand Down Expand Up @@ -328,6 +331,8 @@ Stream consensus phases in real-time over WebSocket.
{"question": "What database should I use?", "rounds": 3}
```

Optional model selection fields: `panel` (list of model refs), `proposer` (model ref), `challengers` (list of model refs).

**Server streams events:**

```json
Expand Down
21 changes: 21 additions & 0 deletions docs/cli/ask.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Output is displayed in real-time with Rich-styled panels:
| `--decompose` | flag | `false` | Decompose the question into subtasks before consensus. |
| `--protocol` | choice | From config (`consensus`) | Protocol: `consensus` (default), `voting`, or `auto` (classify first). |
| `--tools` / `--no-tools` | flag | From config | Enable or disable tool use (web search, code exec, file read). Overrides `tools.enabled` in config. |
| `--proposer` | string | Auto-selected | Override the proposer model (e.g. `anthropic:claude-opus-4-6`). |
| `--challengers` | string | Auto-selected | Override challengers (comma-separated model refs, e.g. `openai:gpt-5.2,google:gemini-3-pro-preview`). |
| `--panel` | string | All models | Restrict consensus to these models only (comma-separated model refs). Overrides `consensus.panel` in config. |

## Examples

Expand Down Expand Up @@ -98,6 +101,24 @@ Disable tool use even if enabled in config:
duh ask --no-tools "Explain the CAP theorem"
```

Use a specific proposer:

```bash
duh ask --proposer openai:gpt-5.2 "Compare REST and GraphQL"
```

Override challengers:

```bash
duh ask --challengers google:gemini-3-pro-preview,anthropic:claude-sonnet-4-6 "Best database for IoT?"
```

Restrict to a panel of models:

```bash
duh ask --panel anthropic:claude-opus-4-6,openai:gpt-5.2 "Design a caching strategy"
```

With a specific config:

```bash
Expand Down
88 changes: 79 additions & 9 deletions docs/cli/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ duh export [OPTIONS] THREAD_ID

## Description

Exports a complete thread including all rounds, contributions, decisions, votes, and metadata. Output can be JSON (for programmatic use) or Markdown (for documentation).
Exports a complete thread including all rounds, contributions, decisions, votes, and metadata. Output can be JSON (for programmatic use), Markdown (for documentation), or PDF (for sharing).

Thread IDs support prefix matching (minimum 8 characters).

See [Export](../export.md) for output format details and scripting examples.
By default, the full report is exported with the decision section first, followed by the complete consensus process. Use `--content decision` to export just the decision.

## Arguments

Expand All @@ -26,27 +26,47 @@ See [Export](../export.md) for output format details and scripting examples.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `--format` | choice | `json` | Export format: `json` or `markdown` |
| `--format` | choice | `json` | Export format: `json`, `markdown`, or `pdf` |
| `--content` | choice | `full` | Content level: `full` report or `decision` only |
| `--no-dissent` | flag | off | Suppress the dissent section |
| `-o`, `--output` | path | stdout | Output file path (required for PDF) |

## Examples

Export as JSON:
Export as JSON (default):

```bash
duh export a1b2c3d4
```

Export as Markdown:
Export as Markdown (full report, decision first):

```bash
duh export a1b2c3d4 --format markdown
```

Save to a file:
Export decision only:

```bash
duh export a1b2c3d4 > thread.json
duh export a1b2c3d4 --format markdown > thread.md
duh export a1b2c3d4 --format markdown --content decision
```

Export decision without dissent:

```bash
duh export a1b2c3d4 --format markdown --content decision --no-dissent
```

Export as PDF:

```bash
duh export a1b2c3d4 --format pdf -o consensus.pdf
```

Save markdown to a file:

```bash
duh export a1b2c3d4 --format markdown -o report.md
```

Extract just the decision with jq:
Expand All @@ -55,8 +75,58 @@ Extract just the decision with jq:
duh export a1b2c3d4 | jq '.turns[-1].decision.content'
```

## Output Formats

### Markdown (full)

```markdown
# Consensus: [question]

## Decision
[decision text]

Confidence: 85%

## Dissent
[dissent text]

---

## Consensus Process

### Round 1

#### Proposal (provider:model)
[proposal text]

#### Challenges
**provider:model**: [challenge text]

#### Revision (provider:model)
[revision text]

---
*duh v0.5.0 | 2026-02-17 | Cost: $0.0030*
```

### Markdown (decision only)

```markdown
# Consensus: [question]

## Decision
[decision text]

Confidence: 85%

## Dissent
[dissent text]

---
*duh v0.5.0 | 2026-02-17 | Cost: $0.0030*
```

## Related

- [Export](../export.md) -- Full export guide with format details
- [`show`](show.md) -- View a thread in the terminal
- [`threads`](threads.md) -- List threads to find IDs
69 changes: 65 additions & 4 deletions docs/concepts/providers-and-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ A provider is an adapter that connects duh to an LLM API. Each provider implemen
- Streaming responses chunk-by-chunk
- Health checks to verify credentials

duh ships with two built-in providers:
duh ships with five built-in providers:

| Provider | API | Models |
|----------|-----|--------|
| **Anthropic** | `api.anthropic.com` | Claude Opus 4.6, Claude Sonnet 4.5, Claude Haiku 4.5 |
| **Anthropic** | `api.anthropic.com` | Claude Opus 4.6, Claude Sonnet 4.6, Claude Sonnet 4.5, Claude Haiku 4.5 |
| **OpenAI** | `api.openai.com` | GPT-5.2, GPT-5 mini, o3 |
| **Google** | `generativelanguage.googleapis.com` | Gemini 3 Pro Preview, Gemini 3 Flash Preview, Gemini 2.5 Flash |
| **Mistral** | `api.mistral.ai` | Mistral Large, Mistral Medium, Mistral Small, Codestral |
| **Perplexity** | `api.perplexity.ai` | Sonar Pro, Sonar (challenger-only) |

## Supported models

Expand All @@ -23,6 +26,7 @@ duh ships with two built-in providers:
| Model | Context | Max Output | Input $/Mtok | Output $/Mtok |
|-------|---------|------------|-------------|---------------|
| Claude Opus 4.6 | 200K | 128K | $5.00 | $25.00 |
| Claude Sonnet 4.6 | 200K | 64K | $3.00 | $15.00 |
| Claude Sonnet 4.5 | 200K | 64K | $3.00 | $15.00 |
| Claude Haiku 4.5 | 200K | 64K | $1.00 | $5.00 |

Expand All @@ -34,16 +38,73 @@ duh ships with two built-in providers:
| GPT-5 mini | 400K | 128K | $0.25 | $2.00 |
| o3 | 200K | 100K | $2.00 | $8.00 |

### Google (Gemini)

| Model | Context | Max Output | Input $/Mtok | Output $/Mtok |
|-------|---------|------------|-------------|---------------|
| Gemini 3 Pro Preview | 1M | 65K | $2.00 | $12.00 |
| Gemini 3 Flash Preview | 1M | 65K | $0.50 | $3.00 |
| Gemini 2.5 Flash | 1M | 65K | $0.15 | $0.60 |

### Mistral

| Model | Context | Max Output | Input $/Mtok | Output $/Mtok |
|-------|---------|------------|-------------|---------------|
| Mistral Large | 128K | 32K | $2.00 | $6.00 |
| Mistral Medium | 128K | 32K | $2.70 | $8.10 |
| Mistral Small | 128K | 32K | $0.20 | $0.60 |
| Codestral | 256K | 32K | $0.30 | $0.90 |

### Perplexity (challenger-only)

All Perplexity models are search-grounded and marked as **challenger-only** -- they participate in the CHALLENGE phase but are never selected as proposers.

| Model | Context | Max Output | Input $/Mtok | Output $/Mtok |
|-------|---------|------------|-------------|---------------|
| Sonar Pro | 200K | 8K | $3.00 | $15.00 |
| Sonar | 128K | 8K | $1.00 | $1.00 |

## Model selection strategy

duh automatically selects models for each phase:

**Proposer selection**: The model with the highest output cost per million tokens is chosen as the proposer, using cost as a proxy for capability. With default models, this means Claude Opus 4.6 proposes.
**Proposer selection**: The model with the highest output cost per million tokens is chosen as the proposer, using cost as a proxy for capability. Models marked as `proposer_eligible=False` (e.g. Perplexity's search-grounded models) are excluded from proposer selection. With default models, this means Claude Opus 4.6 proposes.

**Challenger selection**: Models different from the proposer are preferred (cross-model challenges are more effective). Challengers are sorted by output cost (strongest first) and the top N are selected (default: 2). If not enough different models exist, the proposer model fills remaining slots for same-model ensemble.
**Challenger selection**: Models different from the proposer are preferred (cross-model challenges are more effective). Challengers are sorted by output cost (strongest first) and the top N are selected (default: 2). If not enough different models exist, the proposer model fills remaining slots for same-model ensemble. All models (including challenger-only models) can participate as challengers.

**Reviser**: Always the same model that proposed, since it's revising its own work.

## Controlling model selection

You can control which models participate in consensus:

**Panel** -- Restrict to a subset of models. Only models in the panel are considered for both proposer and challenger roles:

```bash
duh ask --panel anthropic:claude-opus-4-6,openai:gpt-5.2 "Your question"
```

Or in config:

```toml
[consensus]
panel = ["anthropic:claude-opus-4-6", "openai:gpt-5.2", "google:gemini-3-pro-preview"]
```

**Proposer override** -- Force a specific model as proposer:

```bash
duh ask --proposer openai:gpt-5.2 "Your question"
```

**Challenger override** -- Force specific challenger models:

```bash
duh ask --challengers google:gemini-3-pro-preview,perplexity:sonar-pro "Your question"
```

These options are also available in the REST API and WebSocket interface.

## Model capabilities

All built-in models support:
Expand Down
4 changes: 4 additions & 0 deletions docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ api_key_env = "GOOGLE_API_KEY"
min_challengers = 2
proposer_strategy = "round_robin"
challenge_types = ["flaw", "alternative", "risk", "devils_advocate"]
# panel = ["anthropic:claude-opus-4-6", "openai:gpt-5.2"] # Restrict to specific models

[tools]
enabled = false # Enable tool-augmented reasoning
Expand Down Expand Up @@ -124,6 +125,9 @@ duh ask --protocol voting "quick judgment call"
duh ask --decompose "complex multi-part question"
duh ask --tools "question needing web search"
duh ask --no-tools "question that should not use tools"
duh ask --panel anthropic:claude-opus-4-6,openai:gpt-5.2 "restrict to specific models"
duh ask --proposer openai:gpt-5.2 "use a specific proposer"
duh ask --challengers google:gemini-3-pro-preview,perplexity:sonar-pro "specific challengers"
```

## Next steps
Expand Down
Loading
Loading