# Migration Guide: pentest-kit to threetears.models

## Overview

pentest-kit migrates its LLM routing, error handling, embedding, and usage tracking to `threetears.models`. The migration simplifies the codebase by replacing hand-rolled provider management with typed adapters and shared infrastructure. After migration, pentest-kit focuses on security tooling, scan orchestration, and interview flows.

**Moves to threetears.models:**
- LLM routing and tier-based model selection (`ModelTier`, `ModelCache`)
- Error identification and friendly messages (`identify_provider`, `friendly_api_error`)
- Embedding service
- Usage tracking (`UsageTracker`, `UsageRecord`)

**Stays in pentest-kit:**
- Tool confirmation gates (user approval before destructive actions)
- Scan orchestration (nmap, nuclei, ffuf pipeline management)
- Report generation (PDF/HTML pentest reports)
- Interview flows (guided questionnaires for scope definition)

## Dependency update

Add to `pyproject.toml`:

```toml
[project]
dependencies = [
    "3tears-models[anthropic,openai,openrouter,voyageai]",
    # ... existing deps
]
```

## Files to delete

### `utils/llm_tracking.py`

Replaced by `UsageTracker` and `UsageRecord`.

**Before:**
```python
from pentest_kit.utils.llm_tracking import track_usage

track_usage(
    model="claude-sonnet-4-6",
    provider="anthropic",
    input_tokens=150,
    output_tokens=80,
    latency_ms=450,
)
```

**After:**
```python
from threetears.models.tracking import UsageTracker, UsageRecord, LlmPurpose

tracker = UsageTracker()
tracker.record(UsageRecord(
    model_name="claude-sonnet-4-6",
    provider_name="anthropic",
    purpose=LlmPurpose.CHAT,
    input_tokens=150,
    output_tokens=80,
    total_tokens=230,
    latency_ms=450,
))
```

### `services/embedding.py`

Replaced by `OpenAIEmbeddingProvider` / `VoyageAIEmbeddingProvider`.

**Before:**
```python
from pentest_kit.services.embedding import get_embedding

vector = await get_embedding(text, model="text-embedding-3-small")
```

**After:**
```python
from threetears.models.providers.openai import OpenAIEmbeddingProvider

provider = OpenAIEmbeddingProvider("text-embedding-3-small", api_key)
result = await provider.embed(text)
vector = result.vector
```

## Files to modify

### `agents/llm_router.py`

Replace hand-rolled model routing with `ModelTier` + `ModelCache` + provider adapters.

**Before:**
```python
from pentest_kit.agents.llm_router import LLMRouter

router = LLMRouter(config)
model = router.select("large")  # returns raw LangChain model
response = await model.ainvoke(messages)
```

**After:**
```python
from threetears.models import ModelCache, ModelTier
from threetears.models.providers.anthropic import AnthropicChatProvider
from threetears.models.providers.openai import OpenAIChatProvider

cache = ModelCache()

def get_provider(tier: ModelTier, config: dict) -> ChatProvider:
    """resolve provider by tier, caching instances."""
    if tier == ModelTier.LARGE:
        provider_name = "anthropic"
        model_name = "claude-sonnet-4-6"
    elif tier == ModelTier.MEDIUM:
        provider_name = "openai"
        model_name = "gpt-4o-mini"
    else:
        provider_name = "openai"
        model_name = "gpt-4o-mini"

    cached = cache.get(provider_name, model_name)
    if cached is not None:
        return cached

    if provider_name == "anthropic":
        provider = AnthropicChatProvider(model_name, config["anthropic_key"])
    else:
        provider = OpenAIChatProvider(model_name, config["openai_key"])

    cache.put(provider_name, model_name, provider)
    return provider

provider = get_provider(ModelTier.LARGE, config)
result = await provider.complete(messages)
```

### `api/chat_handler.py`

Replace inline `_identify_provider` and `_friendly_api_error` with shared implementations.

**Before:**
```python
def _identify_provider(exc):
    if "anthropic" in str(type(exc).__module__):
        return "Anthropic"
    return "the LLM"

def _friendly_api_error(exc):
    provider = _identify_provider(exc)
    return f"{provider} had an error. Please try again."
```

**After:**
```python
from threetears.models.errors import identify_provider, friendly_api_error

# drop _identify_provider and _friendly_api_error entirely
# use directly:
except Exception as exc:
    provider = identify_provider(exc)
    message = friendly_api_error(exc)
```

## What stays in pentest-kit

These concerns remain because they are security-tooling-specific:

- **Tool confirmation gates**: User approval prompts before running destructive tools (nmap aggressive scans, exploitation modules). This is application-level safety, not model-layer logic.
- **Scan orchestration**: Pipeline management for nmap, nuclei, ffuf, and custom scanners. Includes scheduling, parallelism, and result aggregation.
- **Report generation**: PDF and HTML pentest report rendering from structured findings. Uses Jinja2 templates and WeasyPrint.
- **Interview flows**: Guided questionnaires for engagement scope definition, target enumeration, and rules of engagement. These are domain-specific conversation patterns.
