The platform now runs on Groq by default, through the OpenAI-compatible chat-completions shape. That shape is not one vendor — Gemini, OpenRouter, Together, vLLM and a local Ollama serve it too — so moving again stays configuration rather than code. Two things in the deleted file were not Anthropic's and would have gone with it silently: withRetry / MaxAttempts / retryBackoff were defined in anthropic.go and CALLED BY openai.go. Deleting the file wholesale would have removed the retry policy of the provider that survived, and nothing in openai.go mentions it, so the loss would have been invisible until the next 429. The policy is a property of this platform's runs, not of a vendor's API; it now lives in retry.go where no provider can carry it off. StreamComplete had the same problem and moves to gateway.go, beside the Streamer interface whose comment already referenced it. Three stale-configuration failures are now refused at startup instead of being ignored. Each was verified firing through the real config.Load(): MODEL_PROVIDER=anthropic — named separately from every other wrong value because it used to be correct. Ignoring it gives a stack that believes it is on Claude while every run goes to Groq and is billed there. ANTHROPIC_API_KEY set while MODEL_API_KEY is empty. Ignoring a key an operator did set is the worst version of this: they fail every run on a missing credential they are looking straight at. A leftover claude-* model id, naming the tier that carries it. This is the check the previous commit's error-detail work was diagnosing: such an id is accepted by this process, rejected by the provider, and 400s on EVERY run. "A model is wrong" does not say which of three lines to edit. Defaults ship as a matched pair. defaultBaseURL and the three tier ids are one decision, not four: an id is only meaningful against the service that serves it, and a Groq id on an OpenAI base URL is the same failure from the other side. The tiers also stop being one model — a tier whose cost does not differ is a distinction that buys nothing. Verified end to end against a stub of the wire, driving the real wiring (config.Load in production mode, gateway.New, StreamComplete): streamed deltas, tool-call decoding, the loopback credential exemption, and usage totalling 150 rather than 190 — the cached-prefix subtraction still holds. gofmt clean, go vet clean, 14/14 non-DB packages pass. httpserver still needs a reachable database. NOT verified: the I7 planted-injection eval. Removing this path removed the only model whose refusal behaviour had been measured against it, so the new default is unproven there until `make eval-live` runs with a real key. The Groq model ids should also be confirmed against Groq's current lineup. Flagged in CLAUDE.md §12 and docs/handover.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PJvibeSc1JYXjatankqM1g
277 lines
12 KiB
Makefile
277 lines
12 KiB
Makefile
# ============================================================================
|
|
# Krow backend — developer tasks
|
|
#
|
|
# Migration commands are thin wrappers over the golang-migrate CLI. The
|
|
# migration FILES are the source of truth for the schema; nothing here or in
|
|
# go-api/ ever issues DDL of its own.
|
|
#
|
|
# brew install golang-migrate # or see github.com/golang-migrate/migrate
|
|
# ============================================================================
|
|
|
|
SHELL := /bin/bash
|
|
.DEFAULT_GOAL := help
|
|
|
|
# Capture APP_ENV as the process environment supplied it, BEFORE .env is
|
|
# included. Make gives an included assignment precedence over the environment,
|
|
# so without this `APP_ENV=production make migrate-down` would silently read
|
|
# `development` out of .env and the guard below would not fire.
|
|
APP_ENV_FROM_ENVIRONMENT := $(APP_ENV)
|
|
|
|
ifneq (,$(wildcard .env))
|
|
include .env
|
|
export
|
|
endif
|
|
|
|
# The environment wins; .env is only the fallback. A command-line override
|
|
# (`make migrate-down APP_ENV=production`) outranks both, as Make intends.
|
|
ifneq (,$(APP_ENV_FROM_ENVIRONMENT))
|
|
APP_ENV := $(APP_ENV_FROM_ENVIRONMENT)
|
|
endif
|
|
|
|
MIGRATIONS_DIR ?= ./migrations
|
|
DATABASE_SCHEMA ?= public
|
|
DATABASE_SSLMODE ?= disable
|
|
|
|
# URL-encode the credentials and the database name. "Krow-force" is mixed-case
|
|
# and hyphenated, and a password can contain anything; neither is safe raw.
|
|
enc = $(shell printf '%s' '$(1)' | python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read(), safe=""))')
|
|
|
|
DB_URL = postgres://$(call enc,$(DATABASE_USER)):$(call enc,$(DATABASE_PASSWORD))@$(DATABASE_HOST):$(DATABASE_PORT)/$(call enc,$(DATABASE_NAME))?sslmode=$(DATABASE_SSLMODE)&search_path=$(DATABASE_SCHEMA)
|
|
MIGRATE = migrate -path $(MIGRATIONS_DIR) -database "$(DB_URL)"
|
|
|
|
PSQL = psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d "$(DATABASE_NAME)"
|
|
|
|
# VERSION identifies a build. It defaults to the current commit (with -dirty if
|
|
# the tree has uncommitted changes), so a stamped build is the default rather
|
|
# than something to remember. Override for a release tag: VERSION=v1.2.0.
|
|
VERSION ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo dev)$(shell git diff --quiet 2>/dev/null || echo -dirty)
|
|
IMAGE ?= doormile/krowbackend:latest
|
|
|
|
# Exported, not just defined: docker-compose.yml reads ${VERSION} and ${IMAGE}
|
|
# from the environment, and a make variable is not in a recipe's environment
|
|
# unless it is exported. Without this the compose build arg falls back to "dev"
|
|
# and every image reports the same version.
|
|
export VERSION
|
|
export IMAGE
|
|
|
|
.PHONY: help
|
|
help: ## Show this help
|
|
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
|
|
|
|
# ── Go ──────────────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: run
|
|
run: ## Run the API against the local database
|
|
cd go-api && go run ./cmd/api
|
|
|
|
.PHONY: build
|
|
build: ## Compile the API to go-api/bin/api
|
|
cd go-api && go build -ldflags="-X main.version=$(VERSION)" -o bin/api ./cmd/api
|
|
|
|
.PHONY: tidy
|
|
tidy: ## go mod tidy
|
|
cd go-api && go mod tidy
|
|
|
|
.PHONY: fmt
|
|
fmt: ## gofmt the module
|
|
cd go-api && gofmt -w ./cmd ./internal
|
|
|
|
.PHONY: vet
|
|
vet: ## go vet the module
|
|
cd go-api && go vet ./...
|
|
|
|
.PHONY: test
|
|
test: ## Run the Go tests
|
|
cd go-api && go test ./...
|
|
|
|
.PHONY: eval
|
|
eval: ## Run the agent eval suites (§9 — run this on every loop, retrieval or prompt change)
|
|
cd go-api && go test ./internal/evals/... -run "Suite|Detector" -v -count=1
|
|
|
|
.PHONY: import-agents
|
|
import-agents: ## Publish the specs in agents/ into a tenant: make import-agents ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make import-agents ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/importagents --dir ../agents --skills ../skills --org "$(ORG)"
|
|
|
|
.PHONY: check-agents
|
|
check-agents: ## Parse every spec in agents/ and report, writing nothing
|
|
cd go-api && go run ./cmd/importagents --dir ../agents --skills ../skills --org check --dry-run
|
|
|
|
.PHONY: eval-live
|
|
eval-live: ## Run the eval suites against the REAL model (needs a key, costs tokens)
|
|
@test -n "$$MODEL_API_KEY" || { \
|
|
echo "eval-live needs MODEL_API_KEY — it calls a real model and costs tokens."; \
|
|
echo "The scripted suites (make eval) are the gate; this is the confirmation."; \
|
|
echo ""; \
|
|
if [ -n "$$ANTHROPIC_API_KEY" ]; then \
|
|
echo "NOTE: ANTHROPIC_API_KEY is set and is no longer read — the Anthropic"; \
|
|
echo " path was removed. Rename it to MODEL_API_KEY, and replace the"; \
|
|
echo " value if it is an Anthropic key."; \
|
|
echo ""; \
|
|
fi; \
|
|
echo "Defaults to Groq. To evaluate somewhere else, point it there:"; \
|
|
echo " MODEL_BASE_URL=https://api.groq.com/openai/v1 \\"; \
|
|
echo " MODEL_API_KEY=... MODEL_BALANCED=<model-id> make eval-live"; \
|
|
exit 1; }
|
|
cd go-api && go test ./internal/evals/ -run "TestLive" -v -count=1 -timeout 10m
|
|
|
|
.PHONY: ingest
|
|
ingest: ## Ingest knowledge/ into a tenant: make ingest ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make ingest ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/ingest --dir ../knowledge --org "$(ORG)"
|
|
|
|
.PHONY: reembed
|
|
reembed: ## Re-embed a tenant's corpus with the current model: make reembed ORG=<slug>
|
|
@test -n "$(ORG)" || { echo "usage: make reembed ORG=<slug>"; exit 1; }
|
|
cd go-api && go run ./cmd/reembed --org "$(ORG)"
|
|
|
|
.PHONY: knowledge
|
|
knowledge: ## Run the retrieval layer's permission tests
|
|
cd go-api && go test ./internal/knowledge/ -v -count=1
|
|
|
|
.PHONY: tools
|
|
tools: ## Show what every agent tool returns against the seeded data
|
|
cd go-api && go test ./internal/tools/ -run TestDumpToolOutput -v
|
|
|
|
.PHONY: check
|
|
check: fmt vet test ## Format, vet and test
|
|
|
|
# ── Database ────────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: db-create
|
|
db-create: ## Create the database if it does not exist (never drops anything)
|
|
@if psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -tAc \
|
|
"SELECT 1 FROM pg_database WHERE datname = '$(DATABASE_NAME)'" | grep -q 1; then \
|
|
echo "database \"$(DATABASE_NAME)\" already exists — nothing to do"; \
|
|
else \
|
|
psql -h $(DATABASE_HOST) -p $(DATABASE_PORT) -U $(DATABASE_USER) -d postgres -c \
|
|
'CREATE DATABASE "$(DATABASE_NAME)"' && echo "created \"$(DATABASE_NAME)\""; \
|
|
fi
|
|
|
|
.PHONY: seed
|
|
seed: ## Load the frontend demo dataset (idempotent upsert in one transaction)
|
|
cd go-api && SEED_FIXTURE_PATH="$(CURDIR)/seed/fixtures/seed.json" go run ./cmd/seed
|
|
|
|
# seed/fixtures/seed.json is GENERATED from krow-demo/src/api/seed.js. Edit the
|
|
# seed module, not the fixture. These two targets are the only supported way to
|
|
# change it; both need the frontend repo checked out beside this one.
|
|
.PHONY: seed-fixture
|
|
seed-fixture: ## Regenerate seed.json from the frontend seed module
|
|
cd "$(CURDIR)/../krow-demo" && npm run seed:fixture
|
|
|
|
.PHONY: seed-fixture-check
|
|
seed-fixture-check: ## Fail if seed.json no longer matches the frontend seed module
|
|
cd "$(CURDIR)/../krow-demo" && npm run seed:check
|
|
|
|
.PHONY: gen-resources
|
|
gen-resources: ## Regenerate domain descriptors from the live schema
|
|
python3 scripts/gen_resources.py > go-api/internal/domain/resources_gen.go
|
|
cd go-api && gofmt -w ./internal/domain
|
|
|
|
# Auth runs before routing, so an unauthenticated probe answers 401 for every
|
|
# path — including ones that do not exist. Verifying a deployment therefore
|
|
# needs a session, and the credentials come from the environment so they never
|
|
# reach argv or shell history:
|
|
#
|
|
# KROW_EMAIL=... KROW_PASSWORD=... make verify-deploy BASE=https://your-host
|
|
#
|
|
# Read-only. Add ARGS=--write to exercise the write paths as well.
|
|
.PHONY: verify-deploy
|
|
verify-deploy: ## Check every endpoint on a deployment: make verify-deploy BASE=<url>
|
|
@python3 scripts/verify-deploy.py "$(or $(BASE),http://127.0.0.1:8080)" $(ARGS)
|
|
|
|
.PHONY: db-health
|
|
db-health: ## Ask the running API for its database health
|
|
@curl -fsS http://$(HTTP_HOST):$(HTTP_PORT)/health | python3 -m json.tool
|
|
|
|
# ── Migrations ──────────────────────────────────────────────────────────────
|
|
|
|
.PHONY: migrate-up
|
|
migrate-up: ## Apply all pending migrations
|
|
$(MIGRATE) up
|
|
|
|
.PHONY: migrate-down
|
|
migrate-down: ## Roll back exactly one migration (guarded outside development)
|
|
@if [ "$(APP_ENV)" != "development" ]; then \
|
|
echo "refusing: migrate-down is only permitted when APP_ENV=development (got '$(APP_ENV)')"; \
|
|
echo "roll back staging/production deliberately, with a reviewed plan and a fresh backup."; \
|
|
exit 1; \
|
|
fi
|
|
$(MIGRATE) down 1
|
|
|
|
.PHONY: migrate-status
|
|
migrate-status: ## Show the currently applied migration version
|
|
@$(MIGRATE) version
|
|
|
|
.PHONY: migrate-new
|
|
migrate-new: ## Create a new migration pair: make migrate-new NAME=add_widgets
|
|
@test -n "$(NAME)" || { echo "usage: make migrate-new NAME=add_widgets"; exit 1; }
|
|
migrate create -ext sql -dir $(MIGRATIONS_DIR) -seq $(NAME)
|
|
|
|
.PHONY: migrate-force
|
|
migrate-force: ## Clear a dirty state: make migrate-force VERSION=1
|
|
@test -n "$(VERSION)" || { echo "usage: make migrate-force VERSION=1"; exit 1; }
|
|
$(MIGRATE) force $(VERSION)
|
|
|
|
# There is deliberately no `migrate-drop` target. golang-migrate's `drop`
|
|
# command deletes every table in the database; it must never be one keystroke
|
|
# away from a developer who meant `down`. Run it by hand if you truly need it.
|
|
|
|
.PHONY: verify-schema
|
|
verify-schema: ## Print the tables, enums, FKs and indexes that exist in the target schema
|
|
@$(PSQL) -f scripts/verify_schema.sql
|
|
|
|
# ── Docker ──────────────────────────────────────────────────────────────────
|
|
#
|
|
# These read infrastructure/.env, NOT the repository-root .env that `make run`
|
|
# uses. Two files on purpose: the root one points at a developer's database
|
|
# from the host, the infrastructure one points at a deployment's database from
|
|
# inside a container, and DATABASE_HOST is different in each.
|
|
|
|
COMPOSE_DIR := infrastructure
|
|
COMPOSE_FILES := -f docker-compose.yml
|
|
# Layer the local database in with: make docker-up LOCAL_DB=1
|
|
ifdef LOCAL_DB
|
|
COMPOSE_FILES += -f docker-compose.local-db.yml
|
|
endif
|
|
COMPOSE = cd $(COMPOSE_DIR) && docker compose $(COMPOSE_FILES)
|
|
|
|
.PHONY: docker-build
|
|
docker-build: ## Build the API image
|
|
$(COMPOSE) build
|
|
|
|
.PHONY: docker-up
|
|
docker-up: ## Start the stack (migrations run first). LOCAL_DB=1 adds PostgreSQL
|
|
$(COMPOSE) up -d --build
|
|
@echo "api: http://127.0.0.1:8080/health"
|
|
@echo "logs: make docker-logs"
|
|
|
|
.PHONY: docker-down
|
|
docker-down: ## Stop the stack, keeping volumes
|
|
$(COMPOSE) down
|
|
|
|
.PHONY: docker-logs
|
|
docker-logs: ## Follow the API logs
|
|
$(COMPOSE) logs -f api
|
|
|
|
.PHONY: docker-ps
|
|
docker-ps: ## Show stack status and health
|
|
$(COMPOSE) ps
|
|
|
|
.PHONY: docker-migrate
|
|
docker-migrate: ## Apply migrations only, without starting the API
|
|
$(COMPOSE) run --rm migrate
|
|
|
|
.PHONY: docker-seed
|
|
docker-seed: ## Load the demo fixture into the deployment's database
|
|
$(COMPOSE) run --rm --entrypoint /usr/local/bin/seed api
|
|
|
|
.PHONY: docker-setpassword
|
|
docker-setpassword: ## Set a password: make docker-setpassword EMAIL=demo@krow.app
|
|
@test -n "$(EMAIL)" || { echo "usage: make docker-setpassword EMAIL=demo@krow.app"; exit 1; }
|
|
$(COMPOSE) run --rm -it --entrypoint /usr/local/bin/setpassword api -email $(EMAIL)
|
|
|
|
.PHONY: docker-config
|
|
docker-config: ## Render the fully resolved compose configuration
|
|
$(COMPOSE) config
|