# Shodan last-resort provider

The `shodan` provider is a **credential-free, last-resort failover provider**. It
routes OpenAI-compatible requests to a pool of open model endpoints (Ollama,
vLLM, llama.cpp, LM Studio, …) that are automatically discovered and validated in
the background. It is meant to be added to a rotation as a `last_resort` entry so
that the rotation can still answer when every normal provider is disabled or
rate limited.

## Credit

The discovery, validation, live-registry, and failover-routing **engine** is
vendored, essentially verbatim, from the **open-router** project by
**Pasquale Minervini (@pminervini)**:

> https://github.com/pminervini/open-router

Full credit for that engine belongs to Pasquale Minervini. It lives at
`aisbf/providers/shodan/open_router_engine.py` with provenance and attribution
in its header, and is kept in sync with upstream rather than edited locally. The
upstream repository carries no LICENSE file; it is included here with
attribution and is **not** relicensed under AISBF's GPL. Only the engine (not its
aiohttp server / CLI layer) is used; AISBF wraps it with a small adapter
(`runtime.py`, `handler.py`).

## How it works

1. On first use, a single process-wide runtime starts the engine's background
   discovery + health loops (Shodan facet queries and/or masscan list imports).
2. Each candidate endpoint is probed and only published after it answers
   `/v1/models`, advertises a text-generation model, and completes a minimal
   Chat Completions and Responses request.
3. A request routed to `shodan` is tried against the validated endpoints in
   latency order, failing over transparently to the next endpoint on error.
4. Discovered upstreams are always queried **without credentials** — client
   `Authorization`/API-key/cookie headers are never forwarded.

## Security note

The embedded engine issues Shodan queries and (optionally) imports masscan scan
results **from the host running AISBF**. Shodan/masscan results are untrusted
Internet hosts. Only scan networks you own or are explicitly authorized to test.
Prefer running discovery on a host and network where this is acceptable.

## Configuring the provider

Add a provider of `type: "shodan"`. It needs no API key. All discovery settings
live under `shodan_config` and are optional (sensible defaults apply):

```json
{
  "id": "shodan",
  "name": "Shodan last-resort",
  "type": "shodan",
  "endpoint": "",
  "api_key_required": false,
  "rate_limit": 0,
  "shodan_config": {
    "shodan_mode": "public",
    "shodan_api_key": null,
    "shodan_max_results": null,
    "masscan_lists": [],
    "discovery_interval": 3600,
    "health_interval": 300,
    "blacklist_duration": 86400,
    "workers": 8,
    "probe_timeout": 5.0,
    "readiness_timeout": 30.0,
    "readiness_model_attempts": 3,
    "upstream_timeout": 300.0
  }
}
```

`shodan_config` keys:

| Key | Default | Meaning |
| --- | --- | --- |
| `shodan_mode` | `auto` | `public` (facet scrape, no key), `api` (needs `shodan_api_key`), or `auto` |
| `shodan_api_key` | `null` | Shodan API key; only used in `api`/`auto` mode; sent only to Shodan |
| `shodan_max_results` | `null` | Cap on Shodan API results |
| `masscan_lists` | `[]` | Paths to masscan `list`-format outputs to import (port 11434) |
| `discovery_interval` | `86400` | Seconds between discovery refreshes (`0` disables the loop) |
| `health_interval` | `86400` | Seconds between liveness passes (`0` disables the loop) |
| `blacklist_duration` | `86400` | Seconds a failing endpoint/model stays quarantined |
| `workers` | `8` | Bounded concurrent probe queue size |
| `upstream_timeout` | `300` | Per-request upstream socket timeout, in seconds |

## Using it as a rotation last-resort

Mark the rotation entry with `"last_resort": true`. The special `auto` model name
means "use whatever the discovery pool currently advertises" (recommended, since
discovered model names change over time):

```json
{
  "model_name": "lisa",
  "providers": [
    { "provider_id": "codex",       "weight": 10, "enabled": true,
      "models": [{ "name": "gpt-5.6-luna" }] },
    { "provider_id": "kilo-stefy",  "weight": 3,  "enabled": true,
      "models": [{ "name": "kilo-auto/free" }] },

    { "provider_id": "shodan", "last_resort": true,
      "models": [{ "name": "auto" }] }
  ]
}
```

Behavior:

- While **any** normal provider (here `codex`, `kilo-stefy`) is available, the
  `shodan` entry is held aside and never selected.
- When **all** normal providers are unavailable (disabled in the rotation via
  `enabled: false`, or in a failure/usage cooldown) — the exact case that
  otherwise returns HTTP 429 — the rotation engages the `last_resort` entries
  instead.
- The last-resort pool is also engaged if all normal providers were initially
  available but every one failed during the request's retry loop.

`last_resort` is a general rotation-entry flag: any provider type can carry it,
not just `shodan`.

## Notes / limitations

- Discovery warms up in the background; immediately after start the pool may be
  empty and `shodan` will report no available upstream (the rotation then behaves
  as if it had no last resort). Give discovery a little time, or seed it with
  `masscan_lists`.
- Because discovered upstreams are heterogeneous open servers, capabilities
  (tools, long context, streaming quirks) vary. It is a best-effort safety net,
  not a quality-guaranteed provider.
