mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
feat: load OpenRouter settings from a local .env
Stdlib parser only; pytest skips it so tests cannot pick up a developer key. The embedding model is openai/text-embedding-3-small via OpenRouter /embeddings.
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
# Copy to .env and fill in. .env is gitignored.
|
||||||
|
OPENROUTER_API_KEY=
|
||||||
|
|
||||||
|
# Prove-budget and `ousast index` embeddings via OpenRouter /embeddings.
|
||||||
|
OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
|
||||||
@@ -29,7 +29,7 @@ from .calibration import (
|
|||||||
)
|
)
|
||||||
from .complexity import map as complexity_map
|
from .complexity import map as complexity_map
|
||||||
from .complexity.ledger import persist_verdicts
|
from .complexity.ledger import persist_verdicts
|
||||||
from .config import load_config
|
from .config import load_config, load_dotenv
|
||||||
from .findings import StaticFinding, quick_scan_findings, write_findings
|
from .findings import StaticFinding, quick_scan_findings, write_findings
|
||||||
from .fusion import FusionDecision, fuse_findings_dispatch
|
from .fusion import FusionDecision, fuse_findings_dispatch
|
||||||
from .gate import FALSE_POSITIVE_CEILING, RECALL_FLOOR
|
from .gate import FALSE_POSITIVE_CEILING, RECALL_FLOOR
|
||||||
@@ -83,6 +83,7 @@ class ScanOutcome:
|
|||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
load_dotenv()
|
||||||
parser = argparse.ArgumentParser(prog="ousast")
|
parser = argparse.ArgumentParser(prog="ousast")
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ from pathlib import Path
|
|||||||
DEFAULT_VECTOR_STORE = "json-local"
|
DEFAULT_VECTOR_STORE = "json-local"
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
|
||||||
|
"""Load KEY=VALUE lines from .env into os.environ. Stdlib only; never overrides."""
|
||||||
|
if not force and (os.environ.get("PYTEST_CURRENT_TEST") or os.environ.get("OPENULTRASAST_SKIP_DOTENV")):
|
||||||
|
return
|
||||||
|
env_path = path if path is not None else Path(".env")
|
||||||
|
if not env_path.is_file():
|
||||||
|
return
|
||||||
|
for raw in env_path.read_text().splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
key, _, value = line.partition("=")
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip("'").strip('"')
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ModelConfig:
|
class ModelConfig:
|
||||||
ranker: str | None = None
|
ranker: str | None = None
|
||||||
@@ -129,6 +147,7 @@ class ResolvedConfig:
|
|||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path | None = None) -> ResolvedConfig:
|
def load_config(config_path: Path | None = None) -> ResolvedConfig:
|
||||||
|
load_dotenv()
|
||||||
data: dict[str, object] = {}
|
data: dict[str, object] = {}
|
||||||
if config_path is not None and config_path.exists():
|
if config_path is not None and config_path.exists():
|
||||||
with config_path.open("rb") as handle:
|
with config_path.open("rb") as handle:
|
||||||
@@ -177,7 +196,10 @@ def _load_models(value: object) -> ModelConfig:
|
|||||||
|
|
||||||
def _load_embeddings(value: object) -> EmbeddingConfig:
|
def _load_embeddings(value: object) -> EmbeddingConfig:
|
||||||
data = _section(value)
|
data = _section(value)
|
||||||
return EmbeddingConfig(model=_string(data.get("model")), store=_string(data.get("store")) or DEFAULT_VECTOR_STORE)
|
return EmbeddingConfig(
|
||||||
|
model=_string(data.get("model")) or _string(os.environ.get("OPENROUTER_EMBEDDING_MODEL")),
|
||||||
|
store=_string(data.get("store")) or DEFAULT_VECTOR_STORE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_sandbox(value: object) -> SandboxConfig:
|
def _load_sandbox(value: object) -> SandboxConfig:
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,7 @@
|
|||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openultrasast.config import load_config, write_resolved_config
|
from openultrasast.config import load_config, load_dotenv, write_resolved_config
|
||||||
|
|
||||||
|
|
||||||
def test_load_config_reads_toml(tmp_path: Path) -> None:
|
def test_load_config_reads_toml(tmp_path: Path) -> None:
|
||||||
@@ -159,3 +160,21 @@ def test_load_config_reads_regress_image_pins(tmp_path: Path) -> None:
|
|||||||
assert config.complexity.top_k == 20
|
assert config.complexity.top_k == 20
|
||||||
assert config.complexity.max_hunter_hotspots == 8
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
assert config.sandbox.memory_mb == 2048
|
assert config.sandbox.memory_mb == 2048
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_dotenv_sets_missing_keys_only(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||||
|
env_file = tmp_path / ".env"
|
||||||
|
env_file.write_text("OPENROUTER_API_KEY=from-file\nOPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small\n")
|
||||||
|
monkeypatch.setenv("OPENROUTER_API_KEY", "already-set")
|
||||||
|
monkeypatch.delenv("OPENROUTER_EMBEDDING_MODEL", raising=False)
|
||||||
|
|
||||||
|
load_dotenv(env_file, force=True)
|
||||||
|
|
||||||
|
assert os.environ["OPENROUTER_API_KEY"] == "already-set"
|
||||||
|
assert os.environ["OPENROUTER_EMBEDDING_MODEL"] == "openai/text-embedding-3-small"
|
||||||
|
|
||||||
|
|
||||||
|
def test_embeddings_model_from_env(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||||
|
monkeypatch.setenv("OPENROUTER_EMBEDDING_MODEL", "openai/text-embedding-3-small")
|
||||||
|
config = load_config(None)
|
||||||
|
assert config.embeddings.model == "openai/text-embedding-3-small"
|
||||||
|
|||||||
Reference in New Issue
Block a user