fix(adapter): pass through hybrid-pricing metadata in /capabilities#326
fix(adapter): pass through hybrid-pricing metadata in /capabilities#326seanhanca wants to merge 44 commits into
Conversation
…support Wire `upstreamStaticBody` through the connector template pipeline (loader interface, JSON schema, admin template route, seed script) so endpoints can send a pre-configured request body to the upstream service. Add the `clickhouse-query` connector template with four endpoints: - /network_prices — static SQL for orchestrator pricing data - /query — dynamic SELECT-only queries with regex + blacklist - /ping — ClickHouse health check - /tables — list tables via SHOW TABLES Include a how-to guide with dashboard visualization example and update the connector catalog. Made-with: Cursor
…ded with basic auth Updated the buildUpstreamRequest and buildUpstreamHeaders functions to ensure that when the connector's authType is set to 'basic', the consumer's Authorization header is not forwarded to the upstream service. This change enhances security by preventing potential credential leaks. Added corresponding tests to verify this behavior.
- Use ?? null instead of || null for upstreamStaticBody to preserve empty strings and match the seed script's nullish coalescing - Remove hardcoded allowedHosts from clickhouse-query.json so host validation derives from upstreamBaseUrl at runtime, avoiding conflicts when users override the base URL - Remove envKey from clickhouse-query.json — a single env var cannot map to two separate Basic-auth secrets (username + password); users configure credentials via the Settings tab UI instead - Add JSDoc docstrings to all exported interfaces and functions in the connector template loader and route handlers Made-with: Cursor
Add a new plugin that provides real-time orchestrator rankings based on latency, stability (swap ratio), and price. Queries ClickHouse via the service gateway connector and supports custom SLA-weighted re-ranking. - REST API: POST /rank (filtered + ranked results), GET /filters (capabilities) - Server-side in-memory cache (10s TTL) to reduce ClickHouse load - SLA scoring engine with user-defined latency/swapRate/price weights - Modern dark-theme UI with capability pills, stat cards, and metric badges - Fallback capabilities for local dev without ClickHouse - API tests, unit tests (cache, query builder, ranking), and E2E specs - SDK client examples (curl + TypeScript) and API reference docs Made-with: Cursor
- Validate null/non-object JSON body before accessing properties (rank route) - Change Cache-Control from public to private on authenticated routes - Derive filters route origin from request.url instead of hardcoded localhost - Exclude rows with null metrics when max filters are applied (ranking) - Clamp negative SLA weights to zero before normalization - Skip FIFO cache eviction when refreshing an existing capability key - Increase MAX_QUERY_ROWS to 1000 to match validateTopN upper bound - Fix ClickHouse JSON response parsing for raw array responses - Use != null checks in hasActiveFilters to preserve zero-valued filters - Add accessible labels (htmlFor/id via useId) to FilterInput and WeightSlider - Prevent all-zero SLA weights by resetting changed key to 1 - Restore focus-visible outline on range inputs for keyboard accessibility - Stabilize useLeaderboard effect deps with useMemo for filters/slaWeights - Use fake timers in cache TTL test for deterministic behavior - Add consumer Auth header forwarding test for basic auth strategy - Update test assertions for null-exclusion filter behavior - Add Playwright config and @playwright/test dependency for E2E tests - Replace waitForTimeout with waitForResponse in E2E specs - Remove duplicate build:umd script from frontend package.json - Remove inline password from ClickHouse curl example (security) - Fix connector catalog numbering gap (19 -> 18) - Document fromFallback field in /filters API response - Add res.ok check in example client and curl version note - Remove platform-specific @rollup/rollup-darwin-arm64 optional dep (CI fix) Made-with: Cursor
Introduce an `"experimental": true` flag in plugin.json that causes new plugins to auto-register as hidden (visibleToUsers: false) during sync. Admins can then add specific preview testers via the admin API, keeping the plugin invisible to all other users. Works identically on local dev and Vercel preview deployments (separate Neon DB branches). - plugin-discovery.ts: read experimental flag, pass through to package data - sync-plugin-registry.ts: set visibleToUsers=false only on first create - bin/preview-plugin.sh: helper script to configure any experimental plugin - docs/experimental-plugin-preview.md: full workflow documentation Made-with: Cursor
…E tests - Remove NEXT_PUBLIC_APP_URL/NEXTAUTH_URL from committed .env (use .env.local) - Patch start.sh to sync NEXT_PUBLIC_APP_URL with SHELL_PORT on every start - Remove hardcoded localhost:3000 from CORS allowlist in next.config.js - Consolidate 7 inline localhost fallbacks to use shared appUrl from lib/env - Fix layout.tsx stale localhost:3001 fallback - Use request.url origin for ClickHouse gateway URL resolution - Add auth headers + credentials to plugin frontend API calls - Make Playwright webServer.url respect PLAYWRIGHT_BASE_URL - Add orchestrator-leaderboard E2E spec (stub + live modes) - Add resolveClickhouseGatewayQueryUrl unit tests Made-with: Cursor
Add persistent DiscoveryPlan model to store parameterized discovery queries for external consumers (signers, SDKs). Uses a dedicated plugin_orchestrator_leaderboard Postgres schema with indexes on teamId and ownerUserId for scoped access. Made-with: Cursor
Extend types.ts with DiscoveryPlan, PlanResults, PlanSortBy types and Zod schemas for create/update validation. Capabilities are validated against the existing alphanumeric pattern. Made-with: Cursor
Create plans.ts with create, list, get, update, delete functions for DiscoveryPlan records. Plans are scoped by teamId or ownerUserId. Includes listEnabledPlans() for the cron refresh path. Made-with: Cursor
Add evaluatePlan() to ranking.ts that applies the full pipeline: filter -> SLA score -> min-gate -> custom sort -> topN. Supports all PlanSortBy options (slaScore, latency, price, swapRate, avail). Made-with: Cursor
…upport Create refresh.ts with: - evaluateAndCache(): lazy evaluation with stale-while-revalidate - refreshAllPlans(): bulk refresh for Vercel Cron warming - startLocalRefreshLoop(): optional setInterval for local dev - In-memory plan result cache with configurable TTL Made-with: Cursor
Create API routes for the Discovery Plan system: - GET/POST /plans — list and create plans - GET/PUT/DELETE /plans/:id — get, update, delete a plan - GET /plans/:id/results — lazy-evaluated cached results - POST /plans/refresh — cron-triggered bulk refresh Made-with: Cursor
Schedule plan refresh every 1 minute so most reads are cache hits. Uses CRON_SECRET auth (same pattern as bff-warm). Made-with: Cursor
Add 3 new test files (29 tests total): - evaluate-plan.test.ts: evaluatePlan() pipeline (filter, SLA, gate, sort, topN) - types-validation.test.ts: Zod schema validation (CreatePlan, UpdatePlan) - refresh.test.ts: evaluateAndCache(), plan cache behavior Made-with: Cursor
The function is synchronous so await import() is invalid. Switch to a top-level static import to fix the Vercel build. Made-with: Cursor
…d webhook guide - Add TabNav component for Leaderboard / Plans tab switching - Add PlansOverviewPage with summary stats, plan cards, endpoint info - Add PlanDetailPage with live-editing config (SLA weights, min score, topN, sort, filters) and "Apply Changes" to see results update - Add EndpointGuide component showing API endpoint URL, copy button, and 3-step webhook setup guide for signer ORCHESTRATOR_DISCOVERY_URL - Add usePlans/usePlanDetail hooks for data fetching and state management - Add frontend API functions: fetchPlans, fetchPlanResults, updatePlan, seedDemoPlans - Add backend POST /plans/seed route for dev-only demo data (4 plans) Made-with: Cursor
The results endpoint returns success({ data: results }) which wraps the
response as { success, data: { data: PlanResults } }. The frontend was
accessing json.data directly, causing "Cannot read properties of
undefined (reading 'totalOrchestrators')" at runtime.
Made-with: Cursor
Migrate orchestrator-leaderboard plugin from hardcoded Tailwind gray/blue classes to NaaP's CSS-variable-based design tokens. Rewrites tailwind.config.js with semantic color mappings, globals.css with :root/.dark CSS variables and .glass-card, and all 7 TSX component files with token-backed utility classes. Also fixes a bug where updating a plan's configuration (e.g. SLA min score) did not refresh the orchestrator results — the PUT handler now invalidates the in-memory planCache entry so the next GET /results forces a fresh evaluation. Made-with: Cursor
… preview Create bin/seed-gateway-connector.ts that seeds the clickhouse-query ServiceConnector, its endpoints, and encrypted upstream credentials (username/password) into SecretVault using Prisma directly — no running app or base-svc required. Add step 3.5 to vercel-build.sh that runs this seed after prisma db push when CLICKHOUSE_QUERY_USERNAME and CLICKHOUSE_QUERY_PASSWORD are set. This enables the orchestrator-leaderboard plugin to query ClickHouse on Vercel preview deployments, matching the local dev experience. Made-with: Cursor
Made-with: Cursor
…ookup The User model has no direct `role` column — roles are in a separate UserRole relation table. Use findFirst with orderBy createdAt instead. Made-with: Cursor
Server-to-server calls from leaderboard routes to the gateway endpoint go through the external URL, which gets intercepted by Vercel's deployment protection SSO on preview deployments. Forward the incoming request's cookie header (contains _vercel_jwt) and also support VERCEL_AUTOMATION_BYPASS_SECRET for cron-initiated calls. Made-with: Cursor
Adds bin/seed-discovery-plans.ts (idempotent) that creates 4 demo plans during Vercel preview/production builds so the leaderboard UI has data to display without manual API calls. Runs as step 3.6 in vercel-build.sh after schema push and gateway connector seed. Made-with: Cursor
… scoping
- Fix auth redirect loop: always clear httpOnly cookie via logout endpoint
before redirecting to /login, preventing middleware loop when DB is
unreachable or session is invalid
- Fix plan visibility: use OR logic in scopeWhere so plans match by either
teamId or ownerUserId (build-time seed sets ownerUserId only)
- Fix seed button hidden on Vercel: remove isLocalhost gate so all
authenticated users can seed demo data
- Fix seed API blocked on Vercel: remove NODE_ENV=production check that
blocked preview deployments
- Fix per-user seeding: use user-scoped billingPlanIds (demo-{userId}-slug)
so each user gets their own isolated set of demo plans without unique
constraint collisions
- Set teamId in build-time seed for proper scope matching
Made-with: Cursor
…dmin-configurable refresh - Make orchestrator-leaderboard a core plugin (isCore: true, remove experimental) - Add LeaderboardConfig Prisma model (singleton, refreshIntervalHours, lastRefreshedAt) - Add global dataset in-memory cache with full-replace strategy and configurable TTL - Add time-gated cron endpoint (hourly Vercel cron, skips if interval not elapsed) - Wire plan evaluation to read from global dataset cache (zero ClickHouse calls when populated) - Add GET /dataset, GET/PUT /dataset/config, POST /dataset/refresh API routes - Add AdminSettings panel in plugin frontend (1h/4h/8h/12h selector, refresh now, admin-only) - Add description field to DiscoveryPlan and plan metadata in results response - Add unit tests for config, global dataset cache, plan evaluation with global dataset - Add e2e tests for admin settings panel and plan results metadata Made-with: Cursor
… error handling - Fix TOCTOU race in updatePlan: use updateMany with scoped where clause - Fix TOCTOU race in deletePlan: use deleteMany with scoped where clause - Consistent nullable field handling (remove ?? undefined for slaWeights/filters) - Add .catch() to fire-and-forget refreshSingle in stale-while-revalidate path - Add .catch() to local dev refresh loop to surface errors - Log per-capability evaluation failures instead of silently swallowing Made-with: Cursor
… guide Adds machine-readable contract and integration playbook so external SDKs, gateways, and signers can build, push, and consume discovery plans without reading the route handlers. - docs/openapi.yaml: OpenAPI 3.1 covering all 10 plugin routes (plans CRUD, plan results, ad-hoc rank, capabilities, dataset, dataset config/refresh, bulk plans refresh, demo seed) with full schemas, error envelope, and three security schemes (JWT, gw_ API key, CRON_SECRET). Validated with @redocly/cli. - docs/how-to-guide.md: end-to-end playbook — get an API key, discover capabilities, build a plan field-by-field, push it, pull executed results, and wire the URLs into a runtime. Includes a full TypeScript example with an upsert helper and Livepeer signer wiring notes. Co-authored-by: Cursor <cursoragent@cursor.com>
…ources Playwright E2E spec covering: - API auth guard tests (sources + audits endpoints deny unauthenticated access) - Stub-mode UI tests (plugin shell loads, stubbed sources/audits APIs return correct data via page.evaluate) - Live-mode tests for Vercel preview: source list validation, audit record shape, admin auth enforcement, refresh endpoint reachability - Build & seed verification: app health check, plugin page load, default source seeding with correct priority order 7 tests pass locally (stub + API guard), 9 skip (live-mode, need credentials) Co-authored-by: Cursor <cursoragent@cursor.com>
…derboard Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # package-lock.json
PR-3 of byoc-payment-fleet-2026-05 plan. Closes the missing surface
identified in the design doc § 2: the BYOC adapter calls
serverless-proxy:8080/train/submit for training jobs, but the proxy
never grew that route. This PR adds it.
Changes:
- server.py: POST /train/submit + GET /train/status/{request_id} routes
that delegate to a provider's train_submit() / train_status() methods.
_resolve_training_provider() picks the fal-ai provider (today the only
one with training support — RunPodProvider has methods too but routes
are fal-only by convention until RunPod training is enabled in
CAPABILITIES_JSON).
- providers/fal_ai.py: train_submit() translates SDK body
({model_id, params}) → fal queue body ({images_data_url, trigger_word,
steps}); returns {request_id, model_id, status_url}. train_status()
polls fal queue status; on COMPLETED fetches full result envelope so
diffusers_lora_file.url is available.
TRAINING_MODELS table is the SOT for base → fal training model mapping.
Today: flux-dev/flux-schnell → fal-ai/flux-lora-fast-training.
Tests (14, all pass):
- 5 fal_ai provider tests (body translation, status pass-through, completion fetch)
- 5 route tests (P4 shape, 502 on provider error, 400 on bad JSON, 400 on missing model_id, status dispatch)
- 2 inference regression tests (existing /inference paths unchanged)
- 2 table tests
Cross-repo dependencies:
- Required by: livepeer/simple-infra PR-8 (SDK consumes orch path)
- Coordinates with: livepeer/go-livepeer feat/remote-signer-byoc-v2
(orch already calls adapter /train; adapter calls this /train/submit)
…url, add prefix-stripping regression Addresses three review findings on PR #315: Imp1 — train_submit was hard-coded to forward only images_data_url, trigger_word, steps, create_masks. Caller-provided fal params like learning_rate, is_style, data_archive_format, resume_from_checkpoint were silently dropped. Now spreads params first and applies setdefault for required keys so callers can pass any fal-supported input. Imp2 — status_url in train_submit's return value could be None when fal's envelope omitted it. Now constructs the canonical queue.fal.run/{model}/requests/{id}/status URL as fallback. Adapter doesn't use the field today but the contract advertises it. Imp3 — added test_inference_model_path_strips_provider_prefix to TestInferenceRegression. The two pre-existing tests didn't exercise the extra-provider routing branch in server.py (line 122-126), which is the only inference-path branch that shares state with the new training routing. Now covered: ensures gemini/X model_id routes to GeminiProvider with the prefix stripped. 15/15 proxy tests pass.
Addresses CodeRabbit's one in-scope comment on PR #315. Mirrors the string-coercion handling already applied to `steps` a few lines above. Background: `bool("false")` returns True in Python because any non- empty string is truthy. Without explicit string handling, a caller passing create_masks="false" would have it flipped to True. The fix maps common string boolean forms ("1"/"true"/"yes"/"on", case- insensitive) to True and everything else to False; non-strings fall back to native bool(). All 15 proxy tests still pass. Note: CodeRabbit posted 12 other comments on this PR that flag pre- existing issues in apps/web-next, plugins/orchestrator-leaderboard, etc. Those files are not touched by PR-3 — CodeRabbit reviewed the whole diff vs main and flagged pre-existing code. Out of scope for this PR.
PR-7 of byoc-payment-fleet-2026-05 plan. Adds opt-in persistence to the BYOC inference-adapter's TrainingJob tracking (design §3.D). Same deviation rationale as go-livepeer PR-6: design specified Redis; filesystem JSON checkpoint gives equivalent recovery for single- instance adapter without a new container dependency. New module: livepeer_adapter/training_store.py - TrainingJobStore wraps a dict with optional filesystem persistence. - Dict-like interface preserves all existing __setitem__/__getitem__/ __contains__/values()/items() callsites in proxy.py. - Atomic disk write on __setitem__ via tempfile.mkstemp + os.replace. - Background snapshot loop (5s default) catches attribute mutations on jobs (status/progress/result/error updates happen via direct attribute assignment in proxy._run_training_job — too many sites to refactor). - sweep_on_startup() loads existing checkpoints, marks in-flight jobs as failed_adapter_restart, rewrites their checkpoints. - TTL cleanup deletes terminal checkpoints alongside in-memory entries. - Corrupt JSON files logged + skipped, not fatal. Wiring: - TRAINING_CHECKPOINT_DIR env var enables persistence. Unset = pure in-memory (backward compat). - TrainingJob gained from_dict() classmethod (inverse of to_dict). - ProxyServer.start() kicks off snapshot loop; ProxyServer.stop() cancels it cleanly. Tests (7, all pass): - test_inmemory_mode_no_persistence_dir (backward compat) - test_disk_write_on_insert - test_sweep_marks_inflight_as_failed (I8 invariant) - test_sweep_skips_corrupt_json - test_sweep_no_checkpoint_dir_is_noop - test_snapshot_loop_persists_attribute_mutations - test_atomic_write_no_leftover_tmp Cross-repo: pairs with go-livepeer PR-6 — orch + adapter both checkpoint to their respective directories. Production deploy will mount tmpfs or hostPath volumes per design §3.D.
…tart tests Addresses three review findings on PR-7 (#316): Q4 — fs I/O on the request thread blocks the event loop. Added TrainingJobStore.aset() async setter that offloads _persist_one to asyncio.to_thread. proxy._handle_train_async now uses aset() on the hot path. Subsequent mutations inside _run_training_job continue to rely on the periodic snapshot loop (no per-mutation overhead). Q3 — TrainingJob.from_dict could silently drop fields added later to to_dict. New test test_training_job_roundtrip_matches exercises the REAL production class (not the test fake) — asserts that from_dict(to_dict(j)).to_dict() == j.to_dict() for a job with all optional fields populated. Q7 — I8 invariant not tested across multiple restarts. New test test_sweep_marker_survives_second_restart creates 3 stores in sequence against the same directory, verifies failed_adapter_restart status is durable across all three sweeps. All 9 tests pass.
PR-B of pricing-metering-design.md.
Adds a 14-function extractor registry that computes the units a job
consumed (megapixels output, video seconds output, characters input,
etc.) and emits them as X-Livepeer-Units-Consumed + X-Livepeer-Units-Kind
on the adapter's response. The orch (PR-A) reads these headers to
debit in domain units instead of wall-clock seconds.
Behavior change: zero for caps without `meter` field in CAPABILITIES_JSON.
Header absent → orch falls back to seconds (current behavior preserved
bit-for-bit). Per-cap opt-in is PR-C.
Files added:
- livepeer_adapter/metering/__init__.py — compute_units(meter, req,
resp, elapsed) → (units, kind). Clamps + fallback handling.
- livepeer_adapter/metering/extractors.py — 14 stock extractors:
- output.image_megapixels, .image_count, .video_seconds,
.audio_seconds, .mesh_count, .text_kilo_tokens
- input.text_kilo_chars, .text_chars, .image_megapixels,
.video_seconds, .video_seconds_total
- special: llm.tokens_io, time.seconds, flat.1
- tests/metering/test_extractors.py — 24 tests covering each extractor,
the registry contract, clamps, and empty-input invariants.
Files modified:
- config.py — CapabilityConfig.meter: Optional[dict]. Parses `meter`
field from CAPABILITIES_JSON. Plumbed through load_saved_capabilities.
- proxy.py — _handle_inference resolves meter from cap config, calls
compute_units, sets headers when units > 0 + kind non-empty.
24/24 pytest pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lities The /capabilities response only emitted 5 fields (name, model_id, capacity, price_per_unit, price_scaling). The CAPABILITIES_JSON env input already carries display_price_usd / display_unit / unit_kind / pixels_per_unit / meter — but CapabilityConfig dropped them on load AND the /capabilities handler projected them out again. Consumer chain: adapter /capabilities → SDK /capabilities → storyboard estimateCost(). With display_price_usd missing at the source, every storyboard create_media job persisted cost_usd_estimated:null, which made get_cost_report total \$0 even when real money was spent. Fix in three spots: 1. CapabilityConfig: add display_price_usd / display_unit / unit_kind / pixels_per_unit as Optional fields 2. load_config (env path) + load_saved_capabilities (file path): plumb the new fields when parsing JSON 3. _handle_list_capabilities: emit them when non-null (kept compact for older capabilities that don't have pricing metadata yet) Verified live: curl http://8.229.77.130:9090/capabilities → 53/53 caps now carry display_price_usd curl https://sdk.daydream.monster/capabilities → 53/64 priced (the 11 unpriced are from a separate orch) storyboard create_media flux-schnell → cost_usd_estimated: 0.0032 Companion fix in simple-infra feat/sdk-capabilities-pricing-passthrough (the SDK's CapabilityItem also stripped the fields before this). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
|
🗃️ Database Migration Detected This PR includes changes to Prisma schema files. Please ensure:
Requesting review from the core team: @livepeer/core |
📝 WalkthroughWalkthroughAdds a full orchestrator-leaderboard plugin: DB schema, backend routes, query/ranking/cache, resolver and source adapters, global dataset refresh with cron, discovery plans CRUD/results, admin sources/config/audits, frontend plugin UI, extensive tests, docs/OpenAPI, seeding/build scripts, and related env/proxy/middleware updates. ChangesOrchestrator Leaderboard — End-to-end stack
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested labels
Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
|
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (25)
apps/web-next/src/__tests__/api/orchestrator-leaderboard.test.ts-72-77 (1)
72-77:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRestore
global.fetchafter each test to avoid cross-suite leakage.Lines 72 and 226 overwrite
global.fetch;vi.restoreAllMocks()won’t restore this direct assignment, so later tests can run against a stale mock.Proposed fix
-import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; ... +const originalFetch = global.fetch; + +afterEach(() => { + global.fetch = originalFetch; +});Also applies to: 226-230
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/__tests__/api/orchestrator-leaderboard.test.ts` around lines 72 - 77, Tests directly reassign global.fetch (in the orchestrator-leaderboard.test suite) which isn't undone by vi.restoreAllMocks(), causing cross-suite leakage; capture the original global.fetch into a variable before mocking and restore it in an afterEach/afterAll, or replace direct assignment with vi.spyOn(global, "fetch")/mockResolvedValue and ensure vi.restoreAllMocks() is called in afterEach; update the mock setup around the existing global.fetch assignment (lines using FIXTURE_CH_RESPONSE) and the cleanup near line 226-230 accordingly so the original fetch is reinstated after each test.apps/web-next/src/app/api/v1/gw/admin/templates/route.ts-22-23 (1)
22-23:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAdd authentication to GET handler.
The
GEThandler exposes admin connector templates (includingsecretRefs,upstreamBaseUrl, and endpoint configurations) without authentication. Every other admin route under/gw/admin/requires authentication viagetAdminContext(request); this endpoint should too.If templates are meant to be publicly accessible, move the endpoint outside the
/admin/path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/gw/admin/templates/route.ts` around lines 22 - 23, The GET handler currently returns admin connector templates without authentication; update the export async function GET to accept the incoming Request, call getAdminContext(request) at the start to enforce authentication (and handle failures by returning the appropriate unauthorized response), then proceed to call loadConnectorTemplates() and return the templates; if these templates should remain public instead, move this route out of the /gw/admin/ namespace instead of adding auth. Ensure you reference GET, getAdminContext(request), and loadConnectorTemplates() when making the change.apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/seed/route.ts-70-72 (1)
70-72:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid collision-prone
billingPlanIdderivation.Using only the first 8 chars of
callerIdcan collide across users and break the “scoped per user” guarantee.Proposed fix
function userPlanId(userId: string, slug: string): string { - return `demo-${userId.slice(0, 8)}-${slug}`; + const stable = Buffer.from(userId).toString('base64url').slice(0, 16); + return `demo-${stable}-${slug}`; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/seed/route.ts` around lines 70 - 72, The userPlanId function is collision-prone because it slices callerId to 8 chars; update userPlanId to derive a collision-safe plan id by either using the full userId or a deterministic hash of the userId (e.g., SHA-256 or other stable hex digest) combined with the slug, ensuring the result remains URL-safe and within any length limits; adjust any callers that expect the old short format if necessary and preserve the `demo-...-slug` pattern in the new identifier.apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/seed/route.ts-100-105 (1)
100-105:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not suppress non-duplicate creation failures.
The blanket
catch {}masks operational/data errors and can returnsuccess: trueeven when seeding partially failed.Proposed fix
try { await createPlan(input, scope); created++; - } catch { - // skip duplicates (race or constraint) + } catch (err) { + const msg = err instanceof Error ? err.message : ''; + if (msg.includes('Unique constraint')) continue; // duplicate race + throw err; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/seed/route.ts` around lines 100 - 105, The current blanket catch around createPlan(input, scope) hides real failures; change it to catch (err) { if (isDuplicateError(err)) { /* skip duplicate */ } else { throw err; } } so only unique-constraint/race duplicates are swallowed and all other errors are rethrown; implement an isDuplicateError helper that checks common indicators (Prisma P2002, Postgres 23505, or error message substrings like "duplicate" / "already exists") and reference createPlan, input, scope and the created counter so created only increments on successful creation.apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/route.ts-49-53 (1)
49-53:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse direct Prisma error code check for duplicate-plan handling.
Matching
"Unique constraint"in error text is brittle and can break on message changes/localization, returning the wrong status for duplicates. Check the error code directly instead, following the pattern used elsewhere in the codebase:} catch (err) { const msg = err instanceof Error ? err.message : 'Failed to create plan'; - if (msg.includes('Unique constraint')) { + if ((err as { code?: string })?.code === 'P2002') { return errors.badRequest('A plan with this billingPlanId already exists'); } return errors.internal(msg); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/route.ts` around lines 49 - 53, Replace the brittle text-match in the catch block with a direct Prisma error-code check: import Prisma (or reference Prisma) and change the condition to detect duplicate-key errors by checking if err is an instance of Prisma.PrismaClientKnownRequestError and err.code === 'P2002', then return errors.badRequest('A plan with this billingPlanId already exists'); keep the existing fallback path for other errors and preserve the err message extraction for the final handling.apps/web-next/src/lib/orchestrator-leaderboard/cache.ts-20-33 (1)
20-33:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard against accidental mutation of cached data across requests.
Both
getCached()(line 32) andsetCached()(line 50) leak mutable references.getCached()returns the internal cached array directly, andsetCached()stores the caller's array without copying. While current code doesn't mutate these rows in place, the pattern allows accidental mutations that could corrupt shared cached state. A shallow copy prevents this footgun.Suggested patch
export function getCached(capability: string): { rows: ClickHouseLeaderboardRow[]; cachedAt: number } | null { stats.hits++; - return { rows: entry.data, cachedAt: entry.cachedAt }; + return { + rows: entry.data.map((row) => ({ ...row })), + cachedAt: entry.cachedAt, + }; } const now = Date.now(); LEADERBOARD_CACHE.set(capability, { - data: rows, + data: rows.map((row) => ({ ...row })), cachedAt: now, expiresAt: now + ttlMs, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/cache.ts` around lines 20 - 33, getCached and setCached leak mutable references to the cached rows (returning entry.data directly and storing the caller's array), so prevent accidental cross-request mutation by storing and returning shallow copies: in setCached (the function that writes into LEADERBOARD_CACHE) save a copy of the provided rows (e.g., [...rows] or rows.slice()) into entry.data, and in getCached return a copy of entry.data instead of the internal array; reference getCached, setCached, LEADERBOARD_CACHE and entry.data when making these changes.apps/web-next/src/lib/orchestrator-leaderboard/plans.ts-20-27 (1)
20-27:⚠️ Potential issue | 🟠 Major | ⚡ Quick winEnforce non-empty scope before any scoped plan CRUD.
The current scope builder returns an empty filter when both
teamIdandownerUserIdare missing, which turns scoped reads/updates/deletes into unscoped operations.🔒 Proposed fix
type PlanScope = { teamId?: string; ownerUserId?: string }; +function assertScoped(scope: PlanScope): void { + if (!scope.teamId && !scope.ownerUserId) { + throw new Error('Plan scope requires teamId or ownerUserId'); + } +} + function scopeWhere(scope: PlanScope) { + assertScoped(scope); const conditions: Record<string, string>[] = []; if (scope.teamId) conditions.push({ teamId: scope.teamId }); if (scope.ownerUserId) conditions.push({ ownerUserId: scope.ownerUserId }); if (conditions.length === 0) return {}; if (conditions.length === 1) return conditions[0]; return { OR: conditions }; } export async function createPlan( input: CreatePlanInput, scope: PlanScope, ): Promise<DiscoveryPlan> { + assertScoped(scope); const row = await prisma.discoveryPlan.create({Also applies to: 49-67, 71-119
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/plans.ts` around lines 20 - 27, scopeWhere currently returns an empty filter when PlanScope has neither teamId nor ownerUserId, allowing unscoped CRUD; change scopeWhere (and any scoped CRUD callers) to enforce a non-empty scope by throwing a clear exception (e.g., "Invalid PlanScope: missing teamId and ownerUserId") when conditions.length === 0 so no query runs without scope; update any functions that perform scoped reads/updates/deletes to call scopeWhere and propagate/handle that error rather than permitting an empty filter.apps/web-next/src/lib/orchestrator-leaderboard/refresh.ts-116-127 (1)
116-127:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDeduplicate in-flight refreshes per plan to prevent refresh stampedes.
Stale cache hits can launch multiple concurrent
refreshSingle()calls for the sameplan.id, which creates avoidable duplicate upstream queries.⚙️ Proposed fix
const planCache = new Map<string, PlanCacheEntry>(); +const inFlightRefreshes = new Map<string, Promise<PlanResults>>(); async function refreshSingle( plan: DiscoveryPlan, authToken: string, requestUrl?: string, cookieHeader?: string | null, ): Promise<PlanResults> { - const results = await evaluate(plan, authToken, requestUrl, cookieHeader); - const now = Date.now(); - planCache.set(plan.id, { - results, - cachedAt: now, - expiresAt: now + CACHE_TTL_MS, - }); - return results; + const existing = inFlightRefreshes.get(plan.id); + if (existing) return existing; + + const p = (async () => { + const results = await evaluate(plan, authToken, requestUrl, cookieHeader); + const now = Date.now(); + planCache.set(plan.id, { + results, + cachedAt: now, + expiresAt: now + CACHE_TTL_MS, + }); + return results; + })(); + + inFlightRefreshes.set(plan.id, p); + try { + return await p; + } finally { + inFlightRefreshes.delete(plan.id); + } }Also applies to: 129-143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/refresh.ts` around lines 116 - 127, Stale cache paths can start concurrent refreshSingle(plan, ...) calls for the same plan.id; add a module-level Map (e.g., inFlightRefreshes: Map<string, Promise<any>>) and use it to deduplicate: before calling refreshSingle check if inFlightRefreshes.has(plan.id) and reuse that promise (return or await it as appropriate), otherwise create the promise = refreshSingle(plan, authToken, requestUrl, cookieHeader), store it with inFlightRefreshes.set(plan.id, promise) and ensure promise.finally(() => inFlightRefreshes.delete(plan.id)) to remove the entry when settled; apply the same dedupe logic for both the background fire-and-forget branch and the synchronous return branch so multiple requests for the same plan.id reuse the in-flight promise instead of launching duplicate upstream work.apps/web-next/src/lib/orchestrator-leaderboard/resolver.ts-296-301 (1)
296-301:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPer-capability metrics are being flattened to one arbitrary source row.
The resolver picks
rows[0]during field selection, then reuses that single merged metric set for every capability. That drops capability-specific values (e.g., price/latency/capacity) and can produce incorrect rankings.You’ll need to merge at capability granularity (or preserve per-cap rows from the winning source) instead of merging once per orchestrator and cloning the same metrics across all capabilities.
Also applies to: 366-378
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/resolver.ts` around lines 296 - 301, The code in resolver.ts iterates sources and uses rows[0] (the variable first) to pick metric values, which flattens per-capability metrics and reuses a single metric set across all capabilities; update the logic in the field-selection loop (the block that reads sourceRows.get(src), const first = rows[0], const val = first[field]) to merge or select metrics at capability granularity: either iterate each capability-specific row in rows and merge them into the result per-capability, or retain the winning source's per-capability rows and copy capability-specific fields from the corresponding row rather than using rows[0]; apply the same fix to the analogous block around the 366-378 section so capability-specific price/latency/capacity values are preserved per capability instead of being cloned across all capabilities.apps/web-next/src/lib/orchestrator-leaderboard/sources/naap-discover.ts-78-86 (1)
78-86:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUnchecked
capabilitiesaccess can fail the whole fetch on malformed upstream rows.Line 79 assumes
r.capabilitiesis always an array. If upstream returns one malformed record,.mapthrows and the entire source fetch fails instead of skipping bad rows.Proposed fix
- for (const r of rawRows) { - const shortCaps = r.capabilities.map(extractCapabilityName); + for (const r of rawRows) { + const caps = Array.isArray((r as { capabilities?: unknown }).capabilities) + ? (r as { capabilities: string[] }).capabilities + : []; + const shortCaps = caps.map(extractCapabilityName); rows.push({ orchUri: r.address, capabilities: shortCaps, score: r.score, recentWork: r.recent_work, lastSeenMs: r.last_seen_ms, }); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/sources/naap-discover.ts` around lines 78 - 86, The loop over rawRows assumes each record's r.capabilities is an array and calling r.capabilities.map(extractCapabilityName) can throw if capabilities is missing or malformed; update the iteration (the for (const r of rawRows) block that builds rows and calls extractCapabilityName) to defensively check Array.isArray(r.capabilities) before mapping, skip or default to an empty array for bad entries, and only push rows when required fields (e.g., r.address and a safe capabilities list) are valid so a single malformed upstream row doesn't abort the whole fetch.apps/web-next/src/lib/orchestrator-leaderboard/sources/naap-pricing.ts-67-75 (1)
67-75:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRow normalization can crash on invalid
orchAddress.Line 68 calls
toLowerCase()unconditionally. A null/undefined/non-string value from upstream will throw and fail the full pricing source fetch.Proposed fix
- const rows: NormalizedOrch[] = rawRows.map((r) => ({ - ethAddress: r.orchAddress.toLowerCase(), - orchUri: undefined, - pricePerUnit: r.priceWeiPerUnit, - pipeline: r.pipeline, - model: r.model, - isWarm: r.isWarm, - capabilities: [`${r.model}`], - })); + const rows: NormalizedOrch[] = rawRows.flatMap((r) => { + if (typeof r.orchAddress !== 'string' || r.orchAddress.length === 0) return []; + return [{ + ethAddress: r.orchAddress.toLowerCase(), + orchUri: undefined, + pricePerUnit: r.priceWeiPerUnit, + pipeline: r.pipeline, + model: r.model, + isWarm: r.isWarm, + capabilities: [`${r.model}`], + }]; + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/sources/naap-pricing.ts` around lines 67 - 75, The mapping that builds rows (rawRows.map -> NormalizedOrch) calls r.orchAddress.toLowerCase() unguarded which will throw on null/undefined/non-string; update the normalization to defensively handle non-string orchAddress by checking typeof r.orchAddress === 'string' (or using String(r.orchAddress) safely) before calling toLowerCase, and set ethAddress to a safe fallback (e.g., undefined or '' ) when invalid; modify the rows construction where ethAddress is assigned so other fields (orchUri, pricePerUnit, pipeline, model, isWarm, capabilities) remain unchanged and the normalization does not crash the pricing source fetch.apps/web-next/src/lib/orchestrator-leaderboard/sources/clickhouse.ts-50-53 (1)
50-53:⚠️ Potential issue | 🟠 Major | ⚡ Quick winCapability parsing drops valid ClickHouse results.
On Line 51, when the response is the standard ClickHouse JSON shape (
{ data: [...] }),json.datais already the row array, but the code then readschData.data, which yieldsundefined. This silently returns an empty capabilities list and prevents downstream per-capability queries.Proposed fix
- const json = await res.json(); - const chData = (json.data ?? json) as { data?: Array<{ capability_name: string }> }; - return (chData.data ?? []).map((row: { capability_name: string }) => row.capability_name); + const json = await res.json(); + const rows = Array.isArray((json as { data?: unknown }).data) + ? ((json as { data: Array<{ capability_name?: string }> }).data) + : Array.isArray(json) + ? (json as Array<{ capability_name?: string }>) + : []; + return rows + .map((row) => row.capability_name) + .filter((cap): cap is string => typeof cap === 'string' && cap.length > 0);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/sources/clickhouse.ts` around lines 50 - 53, The current parsing creates chData from json and then reads chData.data which drops the case where json.data is already the row array; change the logic in the try block (where json, chData and the final .map are used) to normalize rows first: detect if json.data is an Array (use that), else if json itself is an Array use json, else if chData.data is an Array use chData.data, otherwise default to empty array; then map over that rows array and return row.capability_name. Update the variables used in the mapping (json, chData, capability_name) accordingly so the standard ClickHouse `{ data: [...] }` shape is handled correctly.apps/web-next/src/lib/orchestrator-leaderboard/sources/clickhouse.ts-96-118 (1)
96-118:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftPer-capability fetches are fully sequential and can timeout under larger capability sets.
Lines 96-118 execute one network request at a time. With dozens of capabilities, this can exceed API/runtime latency budgets and degrade reliability. Use bounded concurrency (batching/pool) to keep wall-clock time predictable.
Proposed direction (bounded concurrency)
- for (const cap of capabilities) { - try { - const sql = buildLeaderboardSQL(cap, MAX_QUERY_ROWS); - const res = await fetch(url, { - method: 'POST', - headers, - body: sql, - signal: AbortSignal.timeout(15_000), - }); - if (!res.ok) { - rawCaps[cap] = []; - continue; - } - const json = await res.json(); - const rows = parseChRows(json); - rawCaps[cap] = rows; - for (const r of rows) { - allRows.push(chRowToNormalized(r, cap)); - } - } catch { - rawCaps[cap] = []; - } - } + const BATCH_SIZE = 5; + for (let i = 0; i < capabilities.length; i += BATCH_SIZE) { + const batch = capabilities.slice(i, i + BATCH_SIZE); + const settled = await Promise.allSettled( + batch.map(async (cap) => { + const sql = buildLeaderboardSQL(cap, MAX_QUERY_ROWS); + const res = await fetch(url, { + method: 'POST', + headers, + body: sql, + signal: AbortSignal.timeout(15_000), + }); + if (!res.ok) return { cap, rows: [] as ClickHouseLeaderboardRow[] }; + const json = await res.json(); + return { cap, rows: parseChRows(json) }; + }), + ); + + for (const s of settled) { + if (s.status === 'fulfilled') { + rawCaps[s.value.cap] = s.value.rows; + for (const r of s.value.rows) allRows.push(chRowToNormalized(r, s.value.cap)); + } + } + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/lib/orchestrator-leaderboard/sources/clickhouse.ts` around lines 96 - 118, The loop over capabilities in the function that calls buildLeaderboardSQL is performing fetches sequentially (for...of over capabilities) which causes long wall-clock time; change it to use bounded concurrency (e.g., a fixed-size worker pool or batching) so multiple fetches run in parallel but limited (suggest concurrency=6-8), preserving the existing behavior: for each cap call fetch with the same options (including AbortSignal.timeout), on non-ok or errors assign rawCaps[cap] = [] and continue, on success parse json -> parseChRows, push rows into rawCaps[cap] and push chRowToNormalized(r, cap) into allRows; implement this by mapping capabilities to async tasks and executing them in controlled batches or using a small p-limit style helper to run N promises concurrently.containers/livepeer-inference-adapter/src/livepeer_adapter/config.py-132-136 (1)
132-136:⚠️ Potential issue | 🟠 Major | ⚡ Quick winNew pricing metadata is loaded but not persisted back to disk.
Lines 132-136 and 178-182 correctly hydrate
meter/display_*, butsave_capabilities()still writes only base fields, so these values are lost after runtime updates and restart.Suggested persistence fix
- caps = [{"name": c.name, "model_id": c.model_id, "capacity": c.capacity, - "price_per_unit": c.price_per_unit, "price_scaling": c.price_scaling} + caps = [{"name": c.name, "model_id": c.model_id, "capacity": c.capacity, + "price_per_unit": c.price_per_unit, "price_scaling": c.price_scaling, + "meter": c.meter, + "display_price_usd": c.display_price_usd, + "display_unit": c.display_unit, + "unit_kind": c.unit_kind, + "pixels_per_unit": c.pixels_per_unit} for c in self.get_capabilities()]Also applies to: 178-182
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@containers/livepeer-inference-adapter/src/livepeer_adapter/config.py` around lines 132 - 136, The new pricing metadata fields (meter, display_price_usd, display_unit, unit_kind, pixels_per_unit) are hydrated from config but not persisted because save_capabilities() only writes base fields; modify save_capabilities() to include these keys when serializing/writing capabilities (mirror how you read them in the hydrate block), so the in-memory values for meter/display_*/unit_kind/pixels_per_unit are saved to disk and survive restarts (also apply same change for the other hydrate block around lines 178-182 where these fields are set).containers/livepeer-inference-adapter/src/livepeer_adapter/proxy.py-372-401 (1)
372-401:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRuntime capability upserts still drop new pricing metadata.
/capabilitiesGET now emitsdisplay_*andmeter, but/capabilitiesPOST (Line 415-421) does not map those fields intoCapabilityConfig, so runtime-added entries can’t persist or round-trip this metadata.Suggested add-handler patch
cap = CapabilityConfig( name=name, model_id=model_id, capacity=body.get("capacity", 1), price_per_unit=body.get("price_per_unit", 0), price_scaling=body.get("price_scaling", 1_000_000), + meter=body.get("meter"), + display_price_usd=body.get("display_price_usd"), + display_unit=body.get("display_unit"), + unit_kind=body.get("unit_kind"), + pixels_per_unit=body.get("pixels_per_unit"), )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@containers/livepeer-inference-adapter/src/livepeer_adapter/proxy.py` around lines 372 - 401, The POST /capabilities handler is not persisting the new pricing/display/meter fields, so extend the request-to-CapabilityConfig mapping to include display_price_usd, display_unit, unit_kind, pixels_per_unit, and meter before calling the config upsert/creation method (where CapabilityConfig is constructed/updated in the POST handler); ensure you read these optional keys from the incoming JSON and set them on the CapabilityConfig instance (or kwargs/dict) so runtime-added capabilities round-trip the same metadata emitted by the GET handler in proxy.py.containers/livepeer-inference-adapter/Dockerfile-1-6 (1)
1-6:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun the adapter as a non-root user.
Line 1-6 leaves the container running as root, which is a preventable privilege-escalation risk.
Suggested hardening patch
FROM python:3.11-slim WORKDIR /app RUN pip install --no-cache-dir aiohttp COPY src/ /app/ +RUN addgroup --system app && adduser --system --ingroup app app \ + && chown -R app:app /app +USER app ENV PYTHONUNBUFFERED=1 CMD ["python", "-m", "livepeer_adapter.main"]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@containers/livepeer-inference-adapter/Dockerfile` around lines 1 - 6, The Dockerfile currently runs the image as root; change it to create and switch to a non-root user before the final CMD to reduce privilege escalation risk: add commands to create a dedicated user/group (e.g., appuser), set appropriate ownership for WORKDIR (/app) and any runtime files (chown -R appuser:appuser /app), set a minimal HOME if needed, and add USER appuser before the existing CMD ["python", "-m", "livepeer_adapter.main"]; keep the existing WORKDIR and CMD but ensure all pip installs and file copies that require root happen before switching to the non-root user.containers/livepeer-inference-adapter/src/livepeer_adapter/metering/__init__.py-48-64 (1)
48-64:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard numeric config parsing in
compute_units.
float(meter.get("fallback"...))/ clamp casts can raise on malformed values and bubble into request handling. A bad meter config should degrade to defaults, not fail metering (or the response path).Suggested defensive parse
- fallback = float(meter.get("fallback", 1.0)) + try: + fallback = float(meter.get("fallback", 1.0)) + except (TypeError, ValueError): + fallback = 1.0 @@ - if min_units is not None: - units = max(units, float(min_units)) - if max_units is not None: - units = min(units, float(max_units)) + if min_units is not None: + try: + units = max(units, float(min_units)) + except (TypeError, ValueError): + pass + if max_units is not None: + try: + units = min(units, float(max_units)) + except (TypeError, ValueError): + pass🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@containers/livepeer-inference-adapter/src/livepeer_adapter/metering/__init__.py` around lines 48 - 64, The numeric parsing in compute_units is unguarded: calls like float(meter.get("fallback", 1.0)) and float(min_units)/float(max_units) can raise on malformed config and should degrade to safe defaults; update compute_units to parse fallback, min_units, and max_units defensively (e.g., try/except or a small safe_float helper) so invalid values revert to fallback or None and do not raise, and ensure extractor_fn result casting to float is also caught and falls back to the fallback value.containers/livepeer-serverless-proxy/Dockerfile-1-6 (1)
1-6:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun the container as a non-root user.
The image currently runs as root, which increases blast radius if the process is compromised.
🔒 Proposed hardening patch
FROM python:3.11-slim WORKDIR /app RUN pip install --no-cache-dir aiohttp COPY src/ /app/ +RUN addgroup --system app && adduser --system --ingroup app app \ + && chown -R app:app /app +USER app ENV PYTHONUNBUFFERED=1 CMD ["python", "-c", "\nimport asyncio\nfrom serverless_proxy.config import load_config\nfrom serverless_proxy.server import ProxyServer, create_provider, create_extra_providers\ncfg = load_config()\nprovider = create_provider(cfg)\nextra = create_extra_providers(cfg)\nserver = ProxyServer(cfg, provider, extra_providers=extra)\nloop = asyncio.new_event_loop()\nloop.run_until_complete(server.start())\nloop.run_forever()\n"]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@containers/livepeer-serverless-proxy/Dockerfile` around lines 1 - 6, The Dockerfile currently runs the Python process as root; update it to create and switch to a non-root user: keep pip install and other root-required steps (RUN pip install --no-cache-dir aiohttp) before creating a system user (e.g., adduser/appuser), chown the application directory (WORKDIR /app and COPY src/ /app/) to that user, then add a USER appuser (and set HOME if needed) before the CMD so ProxyServer (started by the existing CMD invocation) runs unprivileged; ensure file ownership and permissions allow the new user to run the process and write logs/state if required.packages/database/src/plugin-discovery.ts-122-123 (1)
122-123:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHonor
experimentalwhen setting package visibility.Line 283 always sets
visibleToUsers: true, which bypasses the intended experimental default described in Line 122 and parsed in Line 174.Proposed fix
- visibleToUsers: true, + visibleToUsers: plugin.experimental ? false : true,Also applies to: 174-174, 283-284
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/database/src/plugin-discovery.ts` around lines 122 - 123, The code unconditionally sets visibleToUsers: true during package/plug-in registration, ignoring the experimental flag; update the registration logic that currently assigns visibleToUsers: true so it instead respects the parsed experimental property (use something like visibleToUsers: pkg.experimental ? false : true or visibleToUsers: !pkg.experimental), ensuring the experimental?: boolean field is honored when creating/updating the package record.plugins/orchestrator-leaderboard/frontend/src/hooks/useDatasetConfig.ts-62-79 (1)
62-79:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard
refreshNowagainst concurrent execution.
refreshNowcan be invoked multiple times before the first call completes, which can trigger overlapping backend refreshes and race UI state.🛡️ Proposed fix
- const refreshNow = useCallback(async () => { + const refreshNow = useCallback(async () => { + if (isRefreshing) return; setIsRefreshing(true); try { const result = await triggerDatasetRefresh(); @@ } finally { setIsRefreshing(false); } - }, []); + }, [isRefreshing]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/hooks/useDatasetConfig.ts` around lines 62 - 79, refreshNow can run concurrently because its closure doesn't prevent re-entry; guard it with a reentrancy flag (e.g., a mutable ref like isRefreshingRef) so subsequent calls return early while a refresh is in progress. In the refreshNow implementation (inside useDatasetConfig), check the ref at the top and return if true, set the ref and setIsRefreshing(true) before awaiting work, and clear both the ref and setIsRefreshing(false) in finally; update any hook dependencies accordingly to avoid stale closures. Use the existing symbols refreshNow, setIsRefreshing, and fetchDatasetConfig/triggerDatasetRefresh to locate and apply the change.plugins/orchestrator-leaderboard/frontend/src/globals.css-1-3 (1)
1-3:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix Tailwind at-rule lint failures.
Stylelint is currently rejecting
@tailwindon these lines, which will fail linting/CI until the directives are allowed.🔧 Localized fix option in this file
+/* stylelint-disable-next-line scss/at-rule-no-unknown */ `@tailwind` base; +/* stylelint-disable-next-line scss/at-rule-no-unknown */ `@tailwind` components; +/* stylelint-disable-next-line scss/at-rule-no-unknown */ `@tailwind` utilities;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/globals.css` around lines 1 - 3, Stylelint is flagging the `@tailwind` at-rules (`@tailwind` base, `@tailwind` components, `@tailwind` utilities); fix by either updating Stylelint config to recognize Tailwind by adding these to the at-rule ignore list (e.g., add "tailwind", "apply", "variants", "responsive", "layer" to ignoreAtRules in your stylelint config) or add a targeted inline disable comment immediately above the rules (disable at-rule-no-unknown) so the three `@tailwind` directives are allowed during linting; apply the change near the `@tailwind` base/components/utilities lines.plugins/orchestrator-leaderboard/frontend/src/hooks/usePlanDetail.ts-80-82 (1)
80-82:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear stale results when plan is disabled.
The early return on disabled plans keeps prior
resultsin state, so the page can display outdated results for a now-disabled plan.🧩 Proposed fix
useEffect(() => { - if (!plan?.enabled) return; + if (!plan?.enabled) { + setResults(null); + setResultsLoading(false); + return; + } let cancelled = false; setResultsLoading(true);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/hooks/usePlanDetail.ts` around lines 80 - 82, The effect in usePlanDetail that early-returns when plan?.enabled is false leaves prior results in state; update the useEffect (the one starting with "useEffect(() => { if (!plan?.enabled) return; let cancelled = false;") so that when the plan is disabled you explicitly clear stale results (e.g., call the state setter used for results such as setResults([]) or setResults(null)) before returning, preserving the existing cancelled/cleanup logic.plugins/orchestrator-leaderboard/frontend/src/pages/PlanDetailPage.tsx-49-56 (1)
49-56:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHarden
updateFilternumeric parsing to prevent invalid filter state.At Line 54, invalid/partial numeric input can be stored as
NaN, then persisted indraft.filtersand propagated in plan updates.💡 Proposed fix
+const parseOptionalNumber = (raw: string): number | undefined => { + if (raw.trim() === '') return undefined; + const n = Number(raw); + return Number.isFinite(n) ? n : undefined; +}; + const updateFilter = (key: keyof LeaderboardFilters, value: string) => { const current = draft.filters ?? {}; setDraft({ filters: { ...current, - [key]: value !== '' ? Number(value) : undefined, + [key]: parseOptionalNumber(value), }, }); };Also applies to: 257-266
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/pages/PlanDetailPage.tsx` around lines 49 - 56, updateFilter currently converts string input with Number(value) which can produce NaN and persist invalid filter values; modify updateFilter (and the similar handler around lines 257-266) to parse the numeric input safely (e.g. use Number or parseFloat then check with Number.isFinite or isNaN) and only set the filter key when the parsed value is a valid number, otherwise set undefined; also ensure you merge into the existing draft object instead of replacing other draft fields by spreading the previous draft and updating draft.filters so other properties are preserved.plugins/orchestrator-leaderboard/frontend/src/pages/PlansOverviewPage.tsx-145-149 (1)
145-149:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake plan cards keyboard-accessible and prevent nested action hijacking.
At Line 145, navigation is mouse-only because the container is a plain
<div>withonClick. Also, nested controls in the endpoint section can unintentionally trigger card navigation.💡 Proposed fix
-<div +<div key={plan.id} className="plan-card group" + role="button" + tabIndex={0} onClick={() => navigate(`/plans/${plan.id}`)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + navigate(`/plans/${plan.id}`); + } + }} > ... - <div className="border-t border-[var(--border-color)] pt-3"> + <div + className="border-t border-[var(--border-color)] pt-3" + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > <EndpointGuide planId={plan.id} compact /> </div> </div>Also applies to: 225-227
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/pages/PlansOverviewPage.tsx` around lines 145 - 149, The plan card currently uses a plain div with onClick (the element rendering className "plan-card group" and calling navigate(`/plans/${plan.id}`)), making it inaccessible to keyboard users and causing nested controls (the endpoint controls inside the card) to inadvertently trigger card navigation; replace the clickable div with a semantic focusable element (e.g., a <button> or <a> equivalent component) or add tabIndex and key handlers so Enter/Space trigger the same navigate(`/plans/${plan.id}`) behavior, and ensure nested interactive elements inside the endpoint section stop propagation (or check event.target to ignore clicks from interactive descendants) so their clicks don’t call the card navigation; apply the same changes to the other occurrence around lines 225–227.plugins/orchestrator-leaderboard/frontend/src/pages/LeaderboardPage.tsx-224-238 (1)
224-238:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard numeric filter parsing to avoid
NaNin API payloads.At Line 225, Line 228, Line 231, Line 234, and Line 237, invalid numeric input can become
NaN, which is then treated as an active filter and sent downstream.💡 Proposed fix
+const parseOptionalNumber = (raw: string): number | undefined => { + if (raw.trim() === '') return undefined; + const n = Number(raw); + return Number.isFinite(n) ? n : undefined; +}; ... -<FilterInput label="Min GPU RAM (GB)" value={filters.gpuRamGbMin ?? ''} - onChange={(v) => setFilters({ ...filters, gpuRamGbMin: v !== '' ? Number(v) : undefined })} +<FilterInput label="Min GPU RAM (GB)" value={filters.gpuRamGbMin ?? ''} + onChange={(v) => setFilters({ ...filters, gpuRamGbMin: parseOptionalNumber(v) })} type="number" min={0} step={1} /> -<FilterInput label="Max GPU RAM (GB)" value={filters.gpuRamGbMax ?? ''} - onChange={(v) => setFilters({ ...filters, gpuRamGbMax: v !== '' ? Number(v) : undefined })} +<FilterInput label="Max GPU RAM (GB)" value={filters.gpuRamGbMax ?? ''} + onChange={(v) => setFilters({ ...filters, gpuRamGbMax: parseOptionalNumber(v) })} type="number" min={0} step={1} /> -<FilterInput label="Max Price" value={filters.priceMax ?? ''} - onChange={(v) => setFilters({ ...filters, priceMax: v !== '' ? Number(v) : undefined })} +<FilterInput label="Max Price" value={filters.priceMax ?? ''} + onChange={(v) => setFilters({ ...filters, priceMax: parseOptionalNumber(v) })} type="number" min={0} step={0.001} /> -<FilterInput label="Max Avg Latency (ms)" value={filters.maxAvgLatencyMs ?? ''} - onChange={(v) => setFilters({ ...filters, maxAvgLatencyMs: v !== '' ? Number(v) : undefined })} +<FilterInput label="Max Avg Latency (ms)" value={filters.maxAvgLatencyMs ?? ''} + onChange={(v) => setFilters({ ...filters, maxAvgLatencyMs: parseOptionalNumber(v) })} type="number" min={0} /> -<FilterInput label="Max Swap Ratio" value={filters.maxSwapRatio ?? ''} - onChange={(v) => setFilters({ ...filters, maxSwapRatio: v !== '' ? Number(v) : undefined })} +<FilterInput label="Max Swap Ratio" value={filters.maxSwapRatio ?? ''} + onChange={(v) => setFilters({ ...filters, maxSwapRatio: parseOptionalNumber(v) })} type="number" min={0} max={1} step={0.01} />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plugins/orchestrator-leaderboard/frontend/src/pages/LeaderboardPage.tsx` around lines 224 - 238, The numeric FilterInput onChange handlers (e.g., for filters.gpuRamGbMin, gpuRamGbMax, priceMax, maxAvgLatencyMs, maxSwapRatio) currently call Number(v) which can produce NaN and thus send an active invalid filter; update each onChange in LeaderboardPage.tsx to parse the value safely (use parseFloat/Number and then guard with !isNaN or Number.isFinite) and only set the filter key when the parsed value is a valid number, otherwise set it to undefined (preserving the existing empty-string -> undefined behavior); keep using setFilters({...filters, KEY: valid ? parsedValue : undefined}) for the corresponding keys so NaN is never included in the API payload.
🧹 Nitpick comments (3)
apps/web-next/src/app/api/v1/gw/mcp/route.ts (1)
91-91: 💤 Low valueConsider static import for
appUrl.Using
await import('@/lib/env')inside the request handler adds overhead on everytools/callinvocation. SinceappUrlis a constant export, a top-level static import would be more efficient.♻️ Suggested refactor
At the top of the file (after line 14):
+import { appUrl } from '`@/lib/env`';Then at line 91-93:
- const { appUrl: selfOrigin } = await import('`@/lib/env`'); - const proxyPath = `/api/v1/gw/${encodeURIComponent(connectorSlug)}${path}`; - let url = `${selfOrigin}${proxyPath}`; + const proxyPath = `/api/v1/gw/${encodeURIComponent(connectorSlug)}${path}`; + let url = `${appUrl}${proxyPath}`;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/gw/mcp/route.ts` at line 91, The handler currently does a dynamic import to read appUrl (const { appUrl: selfOrigin } = await import('`@/lib/env`')), which wastes time on every tools/call; replace that dynamic import with a top-level static import of appUrl (import { appUrl } from '`@/lib/env`') and update usages to use appUrl (or rename to selfOrigin where used) so the await/import is removed from the request path; search for the symbol appUrl and the local const selfOrigin in route.ts to locate and update the code.apps/web-next/src/app/api/v1/orchestrator-leaderboard/filters/route.ts (1)
72-74: 💤 Low valueClarify the ClickHouse response structure handling.
The nested
json.data ?? jsonfollowed bychData.data ?? []suggests uncertainty about the response shape. Consider documenting the expected formats or simplifying if only one format is actually returned.📝 Add a comment explaining the response formats
const json = await res.json(); + // ClickHouse gateway may wrap data in { data: [...] } or return it directly const chData = (json.data ?? json) as { data?: Array<{ capability_name: string }> }; capabilities = (chData.data ?? []).map((row: { capability_name: string }) => row.capability_name);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/orchestrator-leaderboard/filters/route.ts` around lines 72 - 74, The handling of the ClickHouse response in route.ts is ambiguous: you first assign const json = await res.json(); then wrap it as const chData = (json.data ?? json) and later read (chData.data ?? []).map(...) — clarify and simplify by documenting the expected response shapes (e.g., whether the API returns { data: [...] } or [...] directly) and updating the logic accordingly; either remove the double-layer fallback if only one shape is valid, or add a short comment above these lines explaining both supported formats and why you use json → chData → chData.data, referencing the variables json, chData and the capabilities assignment so reviewers can find and adjust the parsing behavior.apps/web-next/src/app/api/v1/orchestrator-leaderboard/sources/route.ts (1)
136-139: ⚡ Quick winUse
issuesinstead oferrorsfor Zod validation errors—recommended for forward compatibility.While
.errorsworks in Zod 3.23 (the currently installed version), it's deprecated in favor of.issuesand will be removed in Zod 4. Adopting.issuesnow aligns with Zod's standard API and ensures forward compatibility.Proposed improvement
} catch (err) { if (err instanceof z.ZodError) { + const message = err.issues[0]?.message ?? 'Invalid request body'; return NextResponse.json( - { success: false, error: { code: 'VALIDATION_ERROR', message: err.errors[0].message } }, + { success: false, error: { code: 'VALIDATION_ERROR', message } }, { status: 400 }, ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web-next/src/app/api/v1/orchestrator-leaderboard/sources/route.ts` around lines 136 - 139, Replace usage of the deprecated ZodError.errors with ZodError.issues in the validation error handling: in the block checking "if (err instanceof z.ZodError)" update the payload to read from err.issues (e.g., use err.issues[0].message or map issues for full details) before calling NextResponse.json so the response uses the forward-compatible .issues API.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 09b3fd54-6165-4b57-b203-3fcd7cabc179
⛔ Files ignored due to path filters (10)
containers/livepeer-serverless-proxy/src/serverless_proxy/__pycache__/config.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/__pycache__/server.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/__init__.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/base.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/custom.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/fal_ai.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/gemini.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/replicate.cpython-312.pycis excluded by!**/*.pyccontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/__pycache__/runpod.cpython-312.pycis excluded by!**/*.pycpackage-lock.jsonis excluded by!**/package-lock.json,!package-lock.json
📒 Files selected for processing (121)
.gitignoreapps/web-next/.env.local.exampleapps/web-next/next.config.jsapps/web-next/playwright.config.tsapps/web-next/playwright/.auth/admin.jsonapps/web-next/playwright/.auth/user.jsonapps/web-next/src/__tests__/api/orchestrator-leaderboard-global-dataset.test.tsapps/web-next/src/__tests__/api/orchestrator-leaderboard.test.tsapps/web-next/src/app/api/internal/bff-warm/route.tsapps/web-next/src/app/api/v1/auth/providers/[providerSlug]/start/route.tsapps/web-next/src/app/api/v1/gw/admin/templates/route.tsapps/web-next/src/app/api/v1/gw/mcp/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/audits/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/dataset/config/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/dataset/refresh/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/dataset/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/filters/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/[id]/results/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/[id]/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/refresh/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/plans/seed/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/rank/route.tsapps/web-next/src/app/api/v1/orchestrator-leaderboard/sources/route.tsapps/web-next/src/app/api/v1/plugin-publisher/upload/route.tsapps/web-next/src/app/layout.tsxapps/web-next/src/contexts/auth-context.tsxapps/web-next/src/lib/api/auth.tsapps/web-next/src/lib/email.tsapps/web-next/src/lib/gateway/__tests__/transform.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/cache.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/evaluate-plan.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/query.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/ranking.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/refresh.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/resolver.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/sources/naap-discover.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/sources/naap-pricing.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/sources/subgraph.test.tsapps/web-next/src/lib/orchestrator-leaderboard/__tests__/types-validation.test.tsapps/web-next/src/lib/orchestrator-leaderboard/cache.tsapps/web-next/src/lib/orchestrator-leaderboard/config.tsapps/web-next/src/lib/orchestrator-leaderboard/global-dataset.tsapps/web-next/src/lib/orchestrator-leaderboard/global-refresh.tsapps/web-next/src/lib/orchestrator-leaderboard/plans.tsapps/web-next/src/lib/orchestrator-leaderboard/query.tsapps/web-next/src/lib/orchestrator-leaderboard/ranking.tsapps/web-next/src/lib/orchestrator-leaderboard/refresh.tsapps/web-next/src/lib/orchestrator-leaderboard/resolver.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/clickhouse.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/index.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/naap-discover.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/naap-pricing.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/subgraph.tsapps/web-next/src/lib/orchestrator-leaderboard/sources/types.tsapps/web-next/src/lib/orchestrator-leaderboard/types.tsapps/web-next/src/middleware.tsapps/web-next/tests/orchestrator-leaderboard-sources.spec.tsapps/web-next/tests/orchestrator-leaderboard.spec.tsapps/web-next/vercel.jsonbin/preview-plugin.shbin/seed-discovery-plans.tsbin/seed-gateway-connector.tsbin/start.shbin/sync-plugin-registry.tsbin/vercel-build.shcontainers/livepeer-inference-adapter/Dockerfilecontainers/livepeer-inference-adapter/src/livepeer_adapter/config.pycontainers/livepeer-inference-adapter/src/livepeer_adapter/metering/__init__.pycontainers/livepeer-inference-adapter/src/livepeer_adapter/metering/extractors.pycontainers/livepeer-inference-adapter/src/livepeer_adapter/proxy.pycontainers/livepeer-inference-adapter/src/livepeer_adapter/training_store.pycontainers/livepeer-inference-adapter/tests/__init__.pycontainers/livepeer-inference-adapter/tests/metering/__init__.pycontainers/livepeer-inference-adapter/tests/metering/test_extractors.pycontainers/livepeer-inference-adapter/tests/test_training_store.pycontainers/livepeer-serverless-proxy/Dockerfilecontainers/livepeer-serverless-proxy/src/serverless_proxy/providers/fal_ai.pycontainers/livepeer-serverless-proxy/src/serverless_proxy/server.pycontainers/livepeer-serverless-proxy/tests/__init__.pycontainers/livepeer-serverless-proxy/tests/test_training.pydocker/init-schemas.sqldocs/experimental-plugin-preview.mdpackage.jsonpackages/database/prisma/schema.prismapackages/database/src/plugin-discovery.tsplugins/orchestrator-leaderboard/docs/api-reference.mdplugins/orchestrator-leaderboard/docs/data-sources.mdplugins/orchestrator-leaderboard/docs/for-ai.mdplugins/orchestrator-leaderboard/docs/how-to-guide.mdplugins/orchestrator-leaderboard/docs/openapi.yamlplugins/orchestrator-leaderboard/examples/client-test.shplugins/orchestrator-leaderboard/examples/client-test.tsplugins/orchestrator-leaderboard/frontend/e2e/leaderboard.spec.tsplugins/orchestrator-leaderboard/frontend/package.jsonplugins/orchestrator-leaderboard/frontend/playwright.config.tsplugins/orchestrator-leaderboard/frontend/src/App.tsxplugins/orchestrator-leaderboard/frontend/src/components/AdminSettings.tsxplugins/orchestrator-leaderboard/frontend/src/components/EndpointGuide.tsxplugins/orchestrator-leaderboard/frontend/src/components/TabNav.tsxplugins/orchestrator-leaderboard/frontend/src/globals.cssplugins/orchestrator-leaderboard/frontend/src/hooks/useCapabilities.tsplugins/orchestrator-leaderboard/frontend/src/hooks/useDatasetConfig.tsplugins/orchestrator-leaderboard/frontend/src/hooks/useLeaderboard.tsplugins/orchestrator-leaderboard/frontend/src/hooks/usePlanDetail.tsplugins/orchestrator-leaderboard/frontend/src/hooks/usePlans.tsplugins/orchestrator-leaderboard/frontend/src/lib/api.tsplugins/orchestrator-leaderboard/frontend/src/mount.tsxplugins/orchestrator-leaderboard/frontend/src/pages/LeaderboardPage.tsxplugins/orchestrator-leaderboard/frontend/src/pages/PlanDetailPage.tsxplugins/orchestrator-leaderboard/frontend/src/pages/PlansOverviewPage.tsxplugins/orchestrator-leaderboard/frontend/tailwind.config.jsplugins/orchestrator-leaderboard/frontend/tsconfig.jsonplugins/orchestrator-leaderboard/frontend/vite.config.tsplugins/orchestrator-leaderboard/plugin.jsonplugins/service-gateway/connectors/clickhouse-query.jsonplugins/service-gateway/connectors/livepeer-subgraph.jsonplugins/service-gateway/connectors/naap-discover.jsonplugins/service-gateway/connectors/naap-pricing.jsonplugins/service-gateway/docs/clickhouse-query-connector.mdplugins/service-gateway/docs/connector-catalog.md
💤 Files with no reviewable changes (1)
- apps/web-next/next.config.js
| data: | ||
| type: array | ||
| items: { $ref: "#/components/schemas/LeaderboardSource" } | ||
| "400": { $ref: "#/components/responses/ValidationError" } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
python - <<'PY'
from pathlib import Path
import re
p = Path("plugins/orchestrator-leaderboard/docs/openapi.yaml")
text = p.read_text()
refs = set(re.findall(r'\$ref:\s*"`#/components/responses/`([^"]+)"', text))
components_block = re.search(r'components:\n(.*?)(?:\n schemas:)', text, re.S)
defs = set(re.findall(r'^\s{4}([A-Za-z0-9_]+):\s*$', components_block.group(1), re.M)) if components_block else set()
missing = sorted(refs - defs)
print("Referenced responses:", sorted(refs))
print("Defined responses:", sorted(defs))
print("Missing response refs:", missing)
if missing:
raise SystemExit(1)
PYRepository: livepeer/naap
Length of output: 303
Fix unresolved $ref to ValidationError in updateSources 400 response.
Line 575 references #/components/responses/ValidationError, which is not defined in components.responses. The OpenAPI spec will fail validation.
Proposed fix
responses:
"200":
description: Updated source list
content:
application/json:
schema:
allOf:
- $ref: "`#/components/schemas/SuccessEnvelope`"
- type: object
properties:
data:
type: array
items: { $ref: "`#/components/schemas/LeaderboardSource`" }
- "400": { $ref: "`#/components/responses/ValidationError`" }
+ "400":
+ description: Validation failure
+ content:
+ application/json:
+ schema: { $ref: "`#/components/schemas/ErrorEnvelope`" }
"401": { $ref: "`#/components/responses/Unauthorized`" }
"403": { $ref: "`#/components/responses/Forbidden`" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plugins/orchestrator-leaderboard/docs/openapi.yaml` at line 575, The 400
response for updateSources references a missing component
("`#/components/responses/ValidationError`"); add a proper response object named
ValidationError under components.responses (or change the $ref to an existing
response) so the OpenAPI spec validates; ensure the new
components.responses.ValidationError contains a descriptive description and a
JSON schema for the validation error payload consistent with other response
definitions.
|
This PR has conflicts with the base branch. Please rebase to resolve them: git fetch origin
git rebase origin/main
# resolve conflicts, then:
git push --force-with-leaseThe |
What
The inference-adapter's `/capabilities` response only emitted 5 fields (`name`, `model_id`, `capacity`, `price_per_unit`, `price_scaling`). The `CAPABILITIES_JSON` env input already carries the hybrid-pricing display metadata (`display_price_usd`, `display_unit`, `unit_kind`, `pixels_per_unit`, `meter`) but:
Why this matters
Consumer chain: adapter `/capabilities` → SDK `/capabilities` → storyboard MCP `estimateCost()`. With `display_price_usd` missing at the source, every storyboard `create_media` job persisted `cost_usd_estimated: null`, which made `get_cost_report` total $0 even when real GPU money was spent.
Diagnosed during a real marketer brief — agent reported "74/74 jobs missing cost metadata" and blamed the backend. Actually the adapter was the root cause; the SDK was a secondary leak.
The fix
Three small patches in `livepeer-inference-adapter`:
Verification (live, post hot-patch)
```
curl http://8.229.77.130:9090/capabilities → 53/53 caps now carry display_price_usd
flux-dev sample: display_price_usd 0.02625, unit_kind "megapixel", pixels_per_unit 1
```
Then via the SDK (with its own companion fix applied):
```
storyboard create_media flux-schnell → cost_usd_estimated: 0.0032 ✓
```
Companion PR
SDK strips the fields again at `CapabilityItem`, fix in simple-infra `feat/sdk-capabilities-pricing-passthrough`. Both layers need merging.
Deploy state
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
New APIs
Documentation