mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
Add tools/pricing-sync: a standalone, compiled Go tool that fetches
LiteLLM's model_prices_and_context_window.json at build time and
regenerates pricing_data_gen.go, a plain map[string]builtinModelMeta
literal keyed by bare model id (context window, input/output cost in
micro-USD per million tokens).
Pruning rule (documented on the tool's own package doc comment): keep
only entries whose litellm_provider matches this gateway's own provider
types (openai, anthropic, deepseek, xai, minimax, cerebras, dashscope
for qwen/alibaba, and moonshot for Kimi models) and whose mode is chat
or embedding, dropping everything else — a 3111-entry table is a real
startup cost under Yaegi interpretation, so the generated table stays at
236 entries. Bare-id collisions across LiteLLM keys resolve
deterministically: a bare key beats a provider-prefixed one, then
provider priority, then lexicographic order.
Naming bridges: a small, data-driven table (namingBridges) duplicates an
entry under a second bare id when a provider's own live API disagrees
with LiteLLM's naming for the identical model — seeded with MiniMax's
lightning/highspeed dual naming, verified against a live deployment.
Coverage-audit follow-up (verified against a live fetch of the same
source, 2026-08): moonshot's own "litellm_provider" tag carries 23
real Kimi entries (kimi-k2.5, kimi-k3, ...), added to the allowlist as a
first-class provider rather than a per-id bridge, since the table is
already keyed by bare id regardless of provider. Two claims from a
coverage-audit request could NOT be verified against this same fetched
file and were deliberately NOT implemented: no key anywhere in the
3111-entry source ends in ":free" (an openrouter free-tier naming
convention the request assumed exists in this file), and no
"qwen/"-prefixed key exists at all — see the report for the full
grounding note. Also confirmed already-correct by design, no change
needed: audio/responses-mode OpenAI entries ("codex-mini-latest" and
friends) are excluded by the existing mode allowlist, not by any
provider special-case; image/video media models are excluded the same
way.
builtinModelMeta itself lives in its own file (builtin_model_meta.go),
separate from modelmeta.go's resolution logic, so this commit's table
and generator compile standalone.
make pricing-sync regenerates the table; add its gitignore entry for the
tool's own compiled binary.
105 lines
4.4 KiB
Makefile
105 lines
4.4 KiB
Makefile
# Serialize targets even under `make -j`: the integration targets are
|
|
# stateful (one docker compose stack, one set of host ports) and running
|
|
# two of them concurrently would corrupt each other's containers.
|
|
.NOTPARALLEL:
|
|
|
|
.PHONY: test lint yaegi-check admin-ui pricing-sync integration integration-keep integration-up integration-wait integration-down
|
|
|
|
test:
|
|
go test ./... -count=1
|
|
|
|
lint:
|
|
@fmt_out="$$(gofmt -l .)"; \
|
|
if [ -n "$$fmt_out" ]; then \
|
|
echo "gofmt: files not formatted:"; \
|
|
echo "$$fmt_out"; \
|
|
exit 1; \
|
|
fi
|
|
go vet ./...
|
|
|
|
# admin-ui rebuilds the Vue admin panel (webui/) and regenerates
|
|
# admin_assets_gen.go — the repo-root Go file admin.go's serveAdminPage/
|
|
# serveAdminAsset actually serve. Only ever needed after a webui/ source
|
|
# change; CI never runs this (admin_assets_gen.go is committed, and
|
|
# .gitattributes export-ignores webui/ from release source tarballs — see
|
|
# its own comment), so CI never needs node. `npm ci` requires
|
|
# webui/package-lock.json, which is committed for exactly this.
|
|
admin-ui:
|
|
cd webui && npm ci && npm run build
|
|
node webui/generate.mjs
|
|
gofmt -w admin_assets_gen.go
|
|
|
|
yaegi-check:
|
|
cd tools/yaegi-check && GOWORK=off go run . $(CURDIR)
|
|
|
|
# pricing-sync regenerates the repo-root pricing_data_gen.go: the
|
|
# built-in, LiteLLM-synced per-model context/cost table feature v0.23's
|
|
# metadata resolution falls back to (modelmeta.go's resolveModelMeta).
|
|
# Fetches LiteLLM's model_prices_and_context_window.json at BUILD TIME
|
|
# only — see tools/pricing-sync/main.go's own doc comment for the
|
|
# pruning rule and naming-bridge mechanism. Not run by CI (network
|
|
# fetch, and the generated table is committed); an operator re-runs this
|
|
# by hand when LiteLLM's own pricing drifts.
|
|
pricing-sync:
|
|
cd tools/pricing-sync && GOWORK=off go run . $(CURDIR)
|
|
gofmt -w pricing_data_gen.go
|
|
|
|
INTEGRATION_COMPOSE := integration/docker-compose.yml
|
|
INTEGRATION_HEALTH_URL := http://localhost:19081/v1/models
|
|
INTEGRATION_ALICE_KEY := sk-int-alice
|
|
|
|
# Overridable so the optional real-upstream smoke test (task 7,
|
|
# INTEGRATION_REAL=1) never hardcodes one operator's personal hostname as
|
|
# the only option: `INTEGRATION_REAL_BASEURL=https://other.example make
|
|
# integration` points it elsewhere. Rendered into
|
|
# integration/traefik/dynamic/dynamic.yml (git-ignored directory) from the
|
|
# checked-in dynamic.yml.tmpl by integration-up, below. Traefik's file
|
|
# provider watches that whole directory (traefik.yml's providers.file.directory),
|
|
# not the single rendered file — see traefik.yml's own comment for why.
|
|
INTEGRATION_REAL_BASEURL ?= https://llmgw.example.com
|
|
|
|
integration-up:
|
|
mkdir -p integration/traefik/dynamic
|
|
sed 's#__INTEGRATION_REAL_BASEURL__#$(INTEGRATION_REAL_BASEURL)#' integration/traefik/dynamic.yml.tmpl > integration/traefik/dynamic/dynamic.yml
|
|
docker compose -f $(INTEGRATION_COMPOSE) up -d --build
|
|
|
|
integration-wait:
|
|
@i=0; \
|
|
until curl -sf -o /dev/null -H "Authorization: Bearer $(INTEGRATION_ALICE_KEY)" $(INTEGRATION_HEALTH_URL); do \
|
|
i=$$((i+1)); \
|
|
if [ $$i -ge 120 ]; then \
|
|
echo "integration: traefik1 did not become healthy within 120s"; \
|
|
docker compose -f $(INTEGRATION_COMPOSE) logs traefik1; \
|
|
exit 1; \
|
|
fi; \
|
|
sleep 1; \
|
|
done
|
|
|
|
integration-down:
|
|
docker compose -f $(INTEGRATION_COMPOSE) down -v
|
|
|
|
# integration brings the stack up, waits for it to answer, runs the suite,
|
|
# then always tears the stack down — pass, fail, or a failure to even come
|
|
# up or become healthy — so a broken run never leaves a stray stack behind.
|
|
# integration-up/-wait run as recipe-body sub-makes (not prerequisites)
|
|
# specifically so a non-zero status from either one still falls through to
|
|
# integration-down instead of Make aborting before teardown ever runs.
|
|
integration:
|
|
@$(MAKE) integration-up; status=$$?; \
|
|
if [ $$status -eq 0 ]; then \
|
|
$(MAKE) integration-wait; status=$$?; \
|
|
fi; \
|
|
if [ $$status -eq 0 ]; then \
|
|
go -C integration test -tags integration -count=1 ./...; status=$$?; \
|
|
fi; \
|
|
$(MAKE) integration-down; \
|
|
exit $$status
|
|
|
|
# integration-keep is the debugging variant: same run, but leaves the
|
|
# compose stack up afterward for inspecting logs/state by hand (including
|
|
# on a bring-up/health failure, deliberately — there's nothing to inspect
|
|
# post-mortem if it tore itself down). Tear it down yourself with
|
|
# `make integration-down` when done.
|
|
integration-keep: integration-up integration-wait
|
|
go -C integration test -tags integration -count=1 ./...
|