From 790a118634bc98b6271ae32631f2a22345922776 Mon Sep 17 00:00:00 2001 From: dylan Date: Mon, 20 Jul 2026 18:34:27 +0000 Subject: [PATCH 1/6] Request resolution tiers in video challenges and price rewards by tier Video challenges now request a resolution tier (720p/1080p/4K, weighted random) via the generation parameters. Verified videos are priced at min(observed, requested) tier, so overshooting a request never pays more and miners whose model can't reach the tier degrade to a lower price instead of failing. Veo C2PA manifests don't expose the model variant, so Veo videos are priced at the cheapest variant able to produce their (tier, audio) combination: 4K rules out veo-lite and floors at veo-fast's 4K rate. Tier/audio price tables are parsed from OpenRouter pricing_skus at module load, with hardcoded fallbacks. - store requested_resolution on challenge outcomes, audio-track presence on media rows (schema migration) - join media onto outcomes so reward stats carry per-generation model/resolution/audio - fall back to model-name pricing for stats predating tiered challenges --- gas/cache/content_manager.py | 14 ++ gas/cache/db/challenge_store.py | 48 +++++- gas/cache/db/media_store.py | 7 +- gas/cache/db/migrations.py | 7 + gas/cache/types.py | 6 + .../generative_challenge_manager.py | 14 +- gas/evaluation/resolution_tiers.py | 94 +++++++++++ gas/evaluation/rewards.py | 147 ++++++++++++++++-- 8 files changed, 310 insertions(+), 27 deletions(-) create mode 100644 gas/evaluation/resolution_tiers.py diff --git a/gas/cache/content_manager.py b/gas/cache/content_manager.py index ca77751a..813ed24c 100644 --- a/gas/cache/content_manager.py +++ b/gas/cache/content_manager.py @@ -216,6 +216,17 @@ def write_miner_media( return None resolution, file_size = extract_media_info(save_path, modality) + + # Audio presence feeds resolution/audio-tiered reward pricing. + has_audio = None + if modality == Modality.VIDEO: + try: + from gas.cache.util.video import get_video_metadata + + has_audio = bool(get_video_metadata(save_path).get("has_audio")) + except Exception as e: + bt.logging.warning(f"Could not detect audio track in {save_path}: {e}") + media_id = self.media.add_media_entry( prompt_id=prompt_id, file_path=save_path, @@ -234,6 +245,7 @@ def write_miner_media( c2pa_verified=c2pa_verified, c2pa_issuer=c2pa_issuer, task_id=task_id, + has_audio=has_audio, ) self.challenges.update_outcome( task_id=task_id, @@ -611,6 +623,7 @@ def record_challenge_outcome( failure_reason: Optional[str] = None, media_id: Optional[str] = None, created_at: Optional[float] = None, + requested_resolution: Optional[str] = None, ) -> bool: return self.challenges.record_outcome( task_id=task_id, @@ -622,6 +635,7 @@ def record_challenge_outcome( failure_reason=failure_reason, media_id=media_id, created_at=created_at, + requested_resolution=requested_resolution, ) def update_challenge_outcome( diff --git a/gas/cache/db/challenge_store.py b/gas/cache/db/challenge_store.py index 7ba999d6..b361254b 100644 --- a/gas/cache/db/challenge_store.py +++ b/gas/cache/db/challenge_store.py @@ -1,8 +1,9 @@ """ChallengeStore — CRUD and reward stats for the generator_challenge_outcomes table.""" +import json import sqlite3 import time -from typing import List, Dict, Optional, Any +from typing import List, Dict, Optional, Any, Tuple import bittensor as bt @@ -10,6 +11,17 @@ from gas.cache.db.connection import ConnectionManager +def _parse_resolution(raw: Optional[str]) -> Optional[Tuple[int, int]]: + """Parse the media table's JSON-encoded (width, height) resolution.""" + if not raw: + return None + try: + data = json.loads(raw) + return (int(data[0]), int(data[1])) + except (ValueError, TypeError, IndexError, json.JSONDecodeError): + return None + + class ChallengeStore: """Data access for the ``generator_challenge_outcomes`` table.""" @@ -31,6 +43,7 @@ def record_outcome( failure_reason: Optional[str] = None, media_id: Optional[str] = None, created_at: Optional[float] = None, + requested_resolution: Optional[str] = None, ) -> bool: """Insert or update a generation challenge outcome.""" try: @@ -41,9 +54,9 @@ def record_outcome( """ INSERT INTO generator_challenge_outcomes ( task_id, uid, hotkey, prompt_id, modality, status, - failure_reason, media_id, created_at, updated_at + failure_reason, media_id, requested_resolution, created_at, updated_at ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(task_id) DO UPDATE SET uid = excluded.uid, hotkey = excluded.hotkey, @@ -52,9 +65,10 @@ def record_outcome( status = excluded.status, failure_reason = excluded.failure_reason, media_id = COALESCE(excluded.media_id, generator_challenge_outcomes.media_id), + requested_resolution = COALESCE(excluded.requested_resolution, generator_challenge_outcomes.requested_resolution), updated_at = excluded.updated_at """, - (task_id, uid, hotkey, prompt_id, modality, status, failure_reason, media_id, created_at, now), + (task_id, uid, hotkey, prompt_id, modality, status, failure_reason, media_id, requested_resolution, created_at, now), ) conn.commit() return True @@ -144,9 +158,11 @@ def get_outcomes_last_n_hours( conn.row_factory = sqlite3.Row cursor = conn.execute( """ - SELECT * FROM generator_challenge_outcomes - WHERE status IN ('verified', 'failed') AND updated_at >= ? - ORDER BY updated_at DESC LIMIT ? + SELECT o.*, m.resolution AS media_resolution, m.has_audio AS media_has_audio + FROM generator_challenge_outcomes o + LEFT JOIN media m ON o.media_id = m.id + WHERE o.status IN ('verified', 'failed') AND o.updated_at >= ? + ORDER BY o.updated_at DESC LIMIT ? """, (cutoff, int(limit)), ) @@ -161,6 +177,13 @@ def get_outcomes_last_n_hours( failure_reason=row["failure_reason"], media_id=row["media_id"], model_name=row["model_name"] if "model_name" in row.keys() else None, + requested_resolution=( + row["requested_resolution"] if "requested_resolution" in row.keys() else None + ), + observed_resolution=_parse_resolution(row["media_resolution"]), + has_audio=( + bool(row["media_has_audio"]) if row["media_has_audio"] is not None else None + ), created_at=row["created_at"], updated_at=row["updated_at"], ) @@ -186,6 +209,7 @@ def get_outcome_stats_last_n_hours( "image_verified": 0, "video_verified": 0, "image_failed": 0, "video_failed": 0, "image_model_names": [], "video_model_names": [], + "video_generations": [], "last_timestamp": outcome.updated_at, } modality = (outcome.modality or "").lower() @@ -199,6 +223,15 @@ def get_outcome_stats_last_n_hours( miner_stats[hotkey]["video_verified"] += 1 if outcome.model_name: miner_stats[hotkey]["video_model_names"].append(outcome.model_name) + miner_stats[hotkey]["video_generations"].append({ + "model_name": outcome.model_name, + "requested_resolution": outcome.requested_resolution, + "observed_resolution": ( + list(outcome.observed_resolution) + if outcome.observed_resolution else None + ), + "has_audio": outcome.has_audio, + }) if outcome.media_id: miner_stats[hotkey]["verified_media_ids"].append(outcome.media_id) elif outcome.status == "failed": @@ -233,6 +266,7 @@ def get_outcome_stats_last_n_hours( "video_failed": vid_f, "video_pass_rate": (vid_v / vid_t) if vid_t > 0 else 0.0, "video_model_names": stats.get("video_model_names", []), + "video_generations": stats.get("video_generations", []), "media_ids": stats["verified_media_ids"], "last_timestamp": stats["last_timestamp"], } diff --git a/gas/cache/db/media_store.py b/gas/cache/db/media_store.py index 7b6bc589..dfce7843 100644 --- a/gas/cache/db/media_store.py +++ b/gas/cache/db/media_store.py @@ -67,6 +67,7 @@ def _row_to_media_entry(row) -> MediaEntry: perceptual_hash=row["perceptual_hash"] if "perceptual_hash" in row.keys() else None, c2pa_verified=bool(row["c2pa_verified"]) if "c2pa_verified" in row.keys() and row["c2pa_verified"] is not None else False, c2pa_issuer=row["c2pa_issuer"] if "c2pa_issuer" in row.keys() else None, + has_audio=bool(row["has_audio"]) if "has_audio" in row.keys() and row["has_audio"] is not None else None, ) # ------------------------------------------------------------------ @@ -125,6 +126,7 @@ def add_media_entry( c2pa_verified: Optional[bool] = None, c2pa_issuer: Optional[str] = None, task_id: Optional[str] = None, + has_audio: Optional[bool] = None, ) -> str: """Add a media entry to the database. Returns the media id.""" media_id = str(uuid.uuid4()) @@ -139,9 +141,9 @@ def add_media_entry( model_name, generation_args, uid, hotkey, verified, failed_verification, rewarded, created_at, mask_path, timestamp, resolution, file_size, format, - perceptual_hash, c2pa_verified, c2pa_issuer, task_id + perceptual_hash, c2pa_verified, c2pa_issuer, task_id, has_audio ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( media_id, prompt_id, file_path, modality.value, media_type.value, source_type.value, @@ -155,6 +157,7 @@ def add_media_entry( 1 if c2pa_verified else 0 if c2pa_verified is not None else None, c2pa_issuer, task_id, + 1 if has_audio else 0 if has_audio is not None else None, ), ) conn.commit() diff --git a/gas/cache/db/migrations.py b/gas/cache/db/migrations.py index e6c4cab0..97bc71ec 100644 --- a/gas/cache/db/migrations.py +++ b/gas/cache/db/migrations.py @@ -60,6 +60,13 @@ "ALTER TABLE media ADD COLUMN clip_embedding BLOB", ], ), + ( + "add_resolution_tier_columns", + [ + "ALTER TABLE generator_challenge_outcomes ADD COLUMN requested_resolution TEXT", + "ALTER TABLE media ADD COLUMN has_audio BOOLEAN", + ], + ), ( "add_prompt_spec_columns", [ diff --git a/gas/cache/types.py b/gas/cache/types.py index 69af6c5a..cf44317e 100644 --- a/gas/cache/types.py +++ b/gas/cache/types.py @@ -86,6 +86,9 @@ class MediaEntry: c2pa_verified: Optional[bool] = False # C2PA validation passed c2pa_issuer: Optional[str] = None # Issuer name if C2PA verified + # Video audio track presence (None = unknown / not applicable) + has_audio: Optional[bool] = None + # Miner challenge tracking task_id: Optional[str] = None @@ -141,6 +144,9 @@ class ChallengeOutcome: failure_reason: Optional[str] = None media_id: Optional[str] = None model_name: Optional[str] = None + requested_resolution: Optional[str] = None # tier requested in the challenge (e.g. "1080p") + observed_resolution: Optional[tuple] = None # (width, height) of the stored media + has_audio: Optional[bool] = None # audio track present in the stored media created_at: float = None updated_at: float = None diff --git a/gas/evaluation/generative_challenge_manager.py b/gas/evaluation/generative_challenge_manager.py index 2bb9720f..7d496a4e 100644 --- a/gas/evaluation/generative_challenge_manager.py +++ b/gas/evaluation/generative_challenge_manager.py @@ -21,6 +21,7 @@ from typing import Dict, Optional from gas.cache.content_manager import ContentManager +from gas.evaluation.resolution_tiers import sample_challenge_tier from gas.protocol.epistula import get_verifier from gas.protocol.validator_requests import query_generative_miner from gas.types import MediaType, MinerType, Modality @@ -141,7 +142,14 @@ async def issue_generative_challenge(self): async def send_generative_request(self, uid: int, prompt_entry, modality: Modality): """Scoring is handled by the callback in GeneratorEvaluator""" - #parameters = {"width": 1024, "height": 1024} + # Video challenges request a resolution tier; rewards are priced at + # min(observed, requested) so overshooting never pays more and miners + # whose model can't reach the tier degrade to a lower price, not failure. + requested_resolution = None + parameters = None + if modality == Modality.VIDEO: + requested_resolution = sample_challenge_tier() + parameters = {"resolution": requested_resolution} async with aiohttp.ClientSession() as session: response_data = await query_generative_miner( @@ -152,7 +160,7 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali prompt=prompt_entry.content, modality=modality, webhook_url=self.generative_callback_url, - parameters=None, + parameters=parameters, total_timeout=self.config.neuron.miner_total_timeout, ) @@ -169,6 +177,7 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali "media_type": MediaType.SYNTHETIC, "status": "pending", "sent_at": time.time(), + "requested_resolution": requested_resolution, } self.content_manager.record_challenge_outcome( task_id=miner_task_id, @@ -177,6 +186,7 @@ async def send_generative_request(self, uid: int, prompt_entry, modality: Modali prompt_id=prompt_entry.id, modality=modality.value, status="pending", + requested_resolution=requested_resolution, ) bt.logging.info( f"Stored challenge task {miner_task_id} for UID {uid}. Total active tasks: {len(self.challenge_tasks)}" diff --git a/gas/evaluation/resolution_tiers.py b/gas/evaluation/resolution_tiers.py new file mode 100644 index 00000000..8656cddb --- /dev/null +++ b/gas/evaluation/resolution_tiers.py @@ -0,0 +1,94 @@ +""" +Resolution tiers for video generation challenges. + +Validators request a tier per video challenge and reward at the tier actually +delivered, capped at the tier requested: reward_tier = min(observed, requested). +Overshooting a request never pays more, and undershooting (model can't reach +the requested tier) degrades gracefully to the lower tier's price instead of +failing the challenge. + +Tier strings match the OpenRouter /videos API resolution values ("720p", +"1080p", "4K") that the reference miner forwards to providers. +""" + +import random +from typing import Optional, Tuple + +# Lowest to highest. "480p" is a catch-all floor for sub-720p output; it is +# never requested in challenges but observed output can classify into it. +TIER_ORDER = ["480p", "720p", "1080p", "4K"] + +_TIER_RANK = {tier: rank for rank, tier in enumerate(TIER_ORDER)} + +# Weighted distribution for sampling the requested tier of a video challenge. +# The knob for dataset resolution mix and how often top-tier capability is probed. +CHALLENGE_TIER_WEIGHTS = { + "720p": 0.3, + "1080p": 0.4, + "4K": 0.3, +} + +# Classification thresholds on the *minimum* dimension so portrait and +# landscape orientations classify identically (e.g. 720x1280 and 1280x720 are +# both 720p). Thresholds sit below nominal values to tolerate provider +# variations like 704 or 2156-pixel encodes. +_TIER_MIN_DIM_THRESHOLDS = [ + ("4K", 2000), # nominal 2160 + ("1080p", 1000), # nominal 1080 + ("720p", 620), # nominal 720 + ("480p", 0), +] + + +def sample_challenge_tier(rng: random.Random = random) -> str: + """Sample a requested resolution tier from the challenge weight distribution.""" + tiers = list(CHALLENGE_TIER_WEIGHTS.keys()) + weights = list(CHALLENGE_TIER_WEIGHTS.values()) + return rng.choices(tiers, weights=weights, k=1)[0] + + +def normalize_tier(tier: Optional[str]) -> Optional[str]: + """Map a tier string to its canonical form ("4k" -> "4K"), or None if unknown.""" + if not tier: + return None + for canonical in TIER_ORDER: + if canonical.lower() == tier.strip().lower(): + return canonical + return None + + +def tier_from_resolution(resolution: Optional[Tuple[int, int]]) -> Optional[str]: + """Classify observed (width, height) pixel dimensions into a tier.""" + if not resolution or len(resolution) != 2: + return None + try: + min_dim = min(int(resolution[0]), int(resolution[1])) + except (TypeError, ValueError): + return None + if min_dim <= 0: + return None + for tier, threshold in _TIER_MIN_DIM_THRESHOLDS: + if min_dim >= threshold: + return tier + return None + + +def effective_tier( + observed_resolution: Optional[Tuple[int, int]], + requested_tier: Optional[str], +) -> Optional[str]: + """ + Tier a video generation is priced at: min(observed, requested). + + Returns None when the observed resolution is unknown — pricing then falls + back to the baseline, since an unverifiable resolution earns no premium. + A missing requested tier (e.g. outcomes recorded before tiered challenges + shipped) prices at the observed tier alone. + """ + observed = tier_from_resolution(observed_resolution) + if observed is None: + return None + requested = normalize_tier(requested_tier) + if requested is None: + return observed + return observed if _TIER_RANK[observed] <= _TIER_RANK[requested] else requested diff --git a/gas/evaluation/rewards.py b/gas/evaluation/rewards.py index 9581f4f4..0210c0bb 100644 --- a/gas/evaluation/rewards.py +++ b/gas/evaluation/rewards.py @@ -1,9 +1,11 @@ import math import time -from typing import Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import bittensor as bt +from gas.evaluation.resolution_tiers import effective_tier + # Model generation cost in USD per second of video (720p, no audio unless noted). # Used to compute reward multipliers: miner gets baseline_ratio * (model_price / baseline_price). # @@ -13,10 +15,10 @@ # # C2PA blindness note: several providers do NOT expose the model variant in their # C2PA manifests. All Veo (any provider) returns None. All Runway proprietary -# models share the same "RunwayML" softwareAgent. We conservatively use the -# cheapest variant as baseline and accept the ambiguity in the reward multiplier. +# models share the same "RunwayML" softwareAgent. Veo videos are priced by a +# resolution/audio floor (see MODEL_TIER_PRICES); other unknowns get the baseline. GENERATOR_MODEL_PRICES: Dict[str, float] = { - # Google Veo family (C2PA: no variant exposed — all return None → baseline) + # Google Veo family (C2PA: no variant exposed — all return None → tier floor) "google/veo-3.1-lite": 0.03, # cheapest Veo — used as baseline "google/veo-3.1-fast": 0.08, "google/veo-3.1": 0.20, @@ -33,6 +35,34 @@ # "RunwayML": 0.05, # re-enable when gen4.5 signature is fixed } +# Per-(resolution tier, audio) USD/s prices for models whose C2PA manifests +# hide the variant (the Veo family). A Veo video is priced at the cheapest +# variant able to produce its observed (tier, audio) combination — a price +# floor. Since challenges request a tier and rewards use +# min(observed, requested), overshooting a request never pays more. +# +# Keys mirror OpenRouter pricing_skus: the un-suffixed duration_seconds SKU is +# the 1080p default; _720p/_4k suffixes are explicit tiers. Defaults below +# mirror live values as of 2026-07; live SKUs are merged on top at module load. +MODEL_TIER_PRICES: Dict[str, Dict[Tuple[str, bool], float]] = { + "google/veo-3.1-lite": { + ("720p", False): 0.03, ("720p", True): 0.05, + ("1080p", False): 0.05, ("1080p", True): 0.08, + }, + "google/veo-3.1-fast": { + ("720p", False): 0.08, ("720p", True): 0.10, + ("1080p", False): 0.10, ("1080p", True): 0.12, + ("4K", False): 0.25, ("4K", True): 0.30, + }, + "google/veo-3.1": { + ("1080p", False): 0.20, ("1080p", True): 0.40, + ("4K", False): 0.40, ("4K", True): 0.60, + }, +} + +# Models priced via the tier floor when C2PA gives model_name=None. +_TIER_FLOOR_FAMILY_PREFIX = "google/veo" + # Cheapest model price — all multipliers are relative to this. _GENERATOR_BASELINE_PRICE: float = min(GENERATOR_MODEL_PRICES.values()) @@ -41,13 +71,33 @@ _LIVE_PRICES_FETCHED = False -def _fetch_openrouter_prices() -> Dict[str, float]: +def _parse_duration_sku_key(key: str) -> Optional[Tuple[str, bool]]: + """Map a duration_seconds SKU key to a (tier, has_audio) pair. + + "duration_seconds_with_audio" -> ("1080p", True) # no suffix = 1080p default + "duration_seconds_without_audio_720p" -> ("720p", False) + "duration_seconds_with_audio_4k" -> ("4K", True) + """ + if "duration_seconds" not in key: + return None + has_audio = "without_audio" not in key + if key.endswith("_720p"): + tier = "720p" + elif key.endswith("_4k"): + tier = "4K" + else: + tier = "1080p" + return tier, has_audio + + +def _fetch_openrouter_prices() -> Tuple[Dict[str, float], Dict[str, Dict[Tuple[str, bool], float]]]: """Pull live video model prices from OpenRouter /videos/models (free, no auth). - Only runs once per process — result is cached at module level.""" + Returns (flat_prices, tier_prices). Only runs once per process — result is + cached at module level.""" global _LIVE_PRICES_FETCHED if _LIVE_PRICES_FETCHED: - return {} + return {}, {} _LIVE_PRICES_FETCHED = True try: @@ -57,35 +107,45 @@ def _fetch_openrouter_prices() -> Dict[str, float]: timeout=10, ) if resp.status_code != 200: - return {} + return {}, {} models = resp.json().get("data", []) prices: Dict[str, float] = {} + tier_prices: Dict[str, Dict[Tuple[str, bool], float]] = {} for m in models: model_id = m.get("id", "") skus = m.get("pricing_skus", {}) or {} # Find the cheapest per-second price for text-to-video candidates = [] + tier_table: Dict[Tuple[str, bool], float] = {} for key, val in skus.items(): if "text_to_video" not in key and "duration_seconds" not in key: continue try: - candidates.append(float(val)) + price = float(val) except (ValueError, TypeError): - pass + continue + candidates.append(price) + tier_key = _parse_duration_sku_key(key) + if tier_key: + tier_table[tier_key] = price if candidates: prices[model_id] = min(candidates) + if tier_table: + tier_prices[model_id] = tier_table bt.logging.info(f"Fetched {len(prices)} live OpenRouter video model prices") - return prices + return prices, tier_prices except Exception as e: bt.logging.debug(f"Could not fetch OpenRouter prices: {e}") - return {} + return {}, {} # Merge live prices on top of defaults (live wins over hardcoded for same key). -_live = _fetch_openrouter_prices() +_live, _live_tiers = _fetch_openrouter_prices() if _live: GENERATOR_MODEL_PRICES = {**GENERATOR_MODEL_PRICES, **_live} _GENERATOR_BASELINE_PRICE = min(GENERATOR_MODEL_PRICES.values()) +if _live_tiers: + MODEL_TIER_PRICES = {**MODEL_TIER_PRICES, **_live_tiers} def _get_model_price(model_name: str) -> float: @@ -118,6 +178,54 @@ def _compute_average_model_multiplier(model_names: list[str]) -> float: return total / len(model_names) +def _get_video_generation_price(generation: Dict[str, Any]) -> float: + """USD/s price for one verified video generation. + + Named models (Seedance et al. expose model_name via C2PA) use their flat + price. C2PA-blind generations (model_name=None — the Veo family) are + priced at the cheapest Veo variant able to produce the video's + (resolution tier, audio) combination, where the tier is + min(observed, requested) so overshooting a challenge request never pays. + An unknown observed resolution earns only the baseline. + """ + model_name = generation.get("model_name") + if model_name: + return _get_model_price(model_name) + + tier = effective_tier( + generation.get("observed_resolution"), + generation.get("requested_resolution"), + ) + if tier is None: + return _GENERATOR_BASELINE_PRICE + + has_audio = bool(generation.get("has_audio")) + candidates = [ + table[(tier, has_audio)] + for model, table in MODEL_TIER_PRICES.items() + if model.startswith(_TIER_FLOOR_FAMILY_PREFIX) and (tier, has_audio) in table + ] + # No variant offers this (tier, audio) — e.g. sub-720p output — so no + # premium can be justified. + return min(candidates) if candidates else _GENERATOR_BASELINE_PRICE + + +def _compute_video_generation_multiplier(generations: List[Dict[str, Any]]) -> float: + """Average price multiplier across a miner's verified video generations. + + Same sqrt taper as _compute_average_model_multiplier, but priced per + generation from (model_name, resolution tier, audio) instead of model + name alone. + """ + if not generations: + return 1.0 + total = 0.0 + for generation in generations: + price = _get_video_generation_price(generation) + total += math.sqrt(price / _GENERATOR_BASELINE_PRICE) + return total / len(generations) + + def get_generator_base_rewards(verification_stats): """ Compute base rewards for generators based on their verification pass rates, @@ -142,6 +250,7 @@ def get_generator_base_rewards(verification_stats): "video_failed": int, "video_pass_rate": float, "video_model_names": list[str], + "video_generations": list[dict], # per-generation model/resolution/audio "media_ids": List[str] } } @@ -177,9 +286,15 @@ def get_generator_base_rewards(verification_stats): video_pass_rate = stats.get("video_pass_rate", 0.0) video_volume = min(video_verified, 10) + max(0.0, math.log2(max(1, video_verified - 9))) video_base = video_pass_rate * video_volume - video_model_mult = _compute_average_model_multiplier( - stats.get("video_model_names", []) - ) + # Prefer per-generation (model, resolution tier, audio) pricing; + # fall back to model names for stats produced before tiered pricing. + video_generations = stats.get("video_generations") + if video_generations: + video_model_mult = _compute_video_generation_multiplier(video_generations) + else: + video_model_mult = _compute_average_model_multiplier( + stats.get("video_model_names", []) + ) video_base *= video_model_mult uid_rewards[uid] = {"image": image_base, "video": video_base} From 73ac09f6b6e562b75820c109183e95f7279ad664 Mon Sep 17 00:00:00 2001 From: dylan Date: Mon, 20 Jul 2026 19:02:03 +0000 Subject: [PATCH 2/6] Lower 4K challenge share to 20% to cap expected miner spend --- gas/evaluation/resolution_tiers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gas/evaluation/resolution_tiers.py b/gas/evaluation/resolution_tiers.py index 8656cddb..f371d219 100644 --- a/gas/evaluation/resolution_tiers.py +++ b/gas/evaluation/resolution_tiers.py @@ -23,9 +23,9 @@ # Weighted distribution for sampling the requested tier of a video challenge. # The knob for dataset resolution mix and how often top-tier capability is probed. CHALLENGE_TIER_WEIGHTS = { - "720p": 0.3, - "1080p": 0.4, - "4K": 0.3, + "720p": 0.35, + "1080p": 0.45, + "4K": 0.20, } # Classification thresholds on the *minimum* dimension so portrait and From 955de4a858a00e2fd9586c24deaac161818e78e9 Mon Sep 17 00:00:00 2001 From: kenobijon Date: Mon, 20 Jul 2026 16:47:40 -0500 Subject: [PATCH 3/6] Rework tiers: video 480p/720p/1080p, images 1K/2K/4K, quoted Seedance pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Video: 4K is no longer a tier — never requested, and 4K output classifies and prices as 1080p. 480p becomes requestable (mix 20/40/40). Veo 4K price entries stay (live SKU merge re-adds them) but are unreachable until 4K returns to the tier list. Images join the tier system: challenges request 1K/2K/4K (min-dimension nominal 1024/2048/4096, mix 40/40/20), outcomes record requested vs observed tier, and image rewards are priced by delivered tier at min(observed, requested) with relative prices 1x/2x/4x (sqrt taper -> 1x/1.41x/2x). Named models (Seedance exposes model_name via C2PA) are priced from explicit per-tier tables (NAMED_MODEL_TIER_PRICES) built from provider quotes — OpenRouter publishes no Seedance SKUs, and the real spread is not pixel-proportional (seedance-2-0 $0.50/$2.00/$2.40 per 5s at 480p/720p/1080p; fast $0.35/$0.76 at 480p/720p). A tier absent from a model's table pays its top priced tier; named models without a table fall back to pixel-ratio scaling of their flat 720p reference. All named-model prices floor at the baseline. --- gas/cache/db/challenge_store.py | 11 +- .../generative_challenge_manager.py | 14 +- gas/evaluation/resolution_tiers.py | 105 +++++++----- gas/evaluation/rewards.py | 149 ++++++++++++++++-- 4 files changed, 218 insertions(+), 61 deletions(-) diff --git a/gas/cache/db/challenge_store.py b/gas/cache/db/challenge_store.py index b361254b..b18ac2a0 100644 --- a/gas/cache/db/challenge_store.py +++ b/gas/cache/db/challenge_store.py @@ -209,7 +209,7 @@ def get_outcome_stats_last_n_hours( "image_verified": 0, "video_verified": 0, "image_failed": 0, "video_failed": 0, "image_model_names": [], "video_model_names": [], - "video_generations": [], + "image_generations": [], "video_generations": [], "last_timestamp": outcome.updated_at, } modality = (outcome.modality or "").lower() @@ -219,6 +219,14 @@ def get_outcome_stats_last_n_hours( miner_stats[hotkey]["image_verified"] += 1 if outcome.model_name: miner_stats[hotkey]["image_model_names"].append(outcome.model_name) + miner_stats[hotkey]["image_generations"].append({ + "model_name": outcome.model_name, + "requested_resolution": outcome.requested_resolution, + "observed_resolution": ( + list(outcome.observed_resolution) + if outcome.observed_resolution else None + ), + }) elif modality == "video": miner_stats[hotkey]["video_verified"] += 1 if outcome.model_name: @@ -262,6 +270,7 @@ def get_outcome_stats_last_n_hours( "image_failed": img_f, "image_pass_rate": (img_v / img_t) if img_t > 0 else 0.0, "image_model_names": stats.get("image_model_names", []), + "image_generations": stats.get("image_generations", []), "video_verified": vid_v, "video_failed": vid_f, "video_pass_rate": (vid_v / vid_t) if vid_t > 0 else 0.0, diff --git a/gas/evaluation/generative_challenge_manager.py b/gas/evaluation/generative_challenge_manager.py index 7d496a4e..bb554c66 100644 --- a/gas/evaluation/generative_challenge_manager.py +++ b/gas/evaluation/generative_challenge_manager.py @@ -142,14 +142,12 @@ async def issue_generative_challenge(self): async def send_generative_request(self, uid: int, prompt_entry, modality: Modality): """Scoring is handled by the callback in GeneratorEvaluator""" - # Video challenges request a resolution tier; rewards are priced at - # min(observed, requested) so overshooting never pays more and miners - # whose model can't reach the tier degrade to a lower price, not failure. - requested_resolution = None - parameters = None - if modality == Modality.VIDEO: - requested_resolution = sample_challenge_tier() - parameters = {"resolution": requested_resolution} + # Challenges request a resolution tier (video: 480p/720p/1080p, + # image: 1K/2K/4K); rewards are priced at min(observed, requested) so + # overshooting never pays more and miners whose model can't reach the + # tier degrade to a lower price, not failure. + requested_resolution = sample_challenge_tier(modality.value) + parameters = {"resolution": requested_resolution} async with aiohttp.ClientSession() as session: response_data = await query_generative_miner( diff --git a/gas/evaluation/resolution_tiers.py b/gas/evaluation/resolution_tiers.py index f371d219..fce3e452 100644 --- a/gas/evaluation/resolution_tiers.py +++ b/gas/evaluation/resolution_tiers.py @@ -1,63 +1,94 @@ """ -Resolution tiers for video generation challenges. +Resolution tiers for generation challenges. -Validators request a tier per video challenge and reward at the tier actually +Validators request a tier per challenge and reward at the tier actually delivered, capped at the tier requested: reward_tier = min(observed, requested). Overshooting a request never pays more, and undershooting (model can't reach the requested tier) degrades gracefully to the lower tier's price instead of failing the challenge. -Tier strings match the OpenRouter /videos API resolution values ("720p", -"1080p", "4K") that the reference miner forwards to providers. +Video tiers ("480p", "720p", "1080p") match the OpenRouter /videos API +resolution values that the reference miner forwards to providers. 4K video is +deliberately not a tier for now: it is never requested, and 4K output +classifies (and prices) as 1080p. + +Image tiers ("1K", "2K", "4K") refer to the minimum pixel dimension +(nominal 1024 / 2048 / 4096). """ import random -from typing import Optional, Tuple +from typing import Dict, List, Optional, Tuple + +VIDEO_MODALITY = "video" +IMAGE_MODALITY = "image" -# Lowest to highest. "480p" is a catch-all floor for sub-720p output; it is -# never requested in challenges but observed output can classify into it. -TIER_ORDER = ["480p", "720p", "1080p", "4K"] +# Lowest to highest, per modality. +TIER_ORDER: Dict[str, List[str]] = { + VIDEO_MODALITY: ["480p", "720p", "1080p"], + IMAGE_MODALITY: ["1K", "2K", "4K"], +} -_TIER_RANK = {tier: rank for rank, tier in enumerate(TIER_ORDER)} +_TIER_RANK: Dict[str, Dict[str, int]] = { + modality: {tier: rank for rank, tier in enumerate(order)} + for modality, order in TIER_ORDER.items() +} -# Weighted distribution for sampling the requested tier of a video challenge. -# The knob for dataset resolution mix and how often top-tier capability is probed. -CHALLENGE_TIER_WEIGHTS = { - "720p": 0.35, - "1080p": 0.45, - "4K": 0.20, +# Weighted distributions for sampling the requested tier of a challenge. +# The knob for dataset resolution mix and how often top-tier capability is +# probed (top tiers are the most expensive for miners to serve). +CHALLENGE_TIER_WEIGHTS: Dict[str, Dict[str, float]] = { + VIDEO_MODALITY: { + "480p": 0.20, + "720p": 0.40, + "1080p": 0.40, + }, + IMAGE_MODALITY: { + "1K": 0.40, + "2K": 0.40, + "4K": 0.20, + }, } # Classification thresholds on the *minimum* dimension so portrait and # landscape orientations classify identically (e.g. 720x1280 and 1280x720 are # both 720p). Thresholds sit below nominal values to tolerate provider -# variations like 704 or 2156-pixel encodes. -_TIER_MIN_DIM_THRESHOLDS = [ - ("4K", 2000), # nominal 2160 - ("1080p", 1000), # nominal 1080 - ("720p", 620), # nominal 720 - ("480p", 0), -] - - -def sample_challenge_tier(rng: random.Random = random) -> str: - """Sample a requested resolution tier from the challenge weight distribution.""" - tiers = list(CHALLENGE_TIER_WEIGHTS.keys()) - weights = list(CHALLENGE_TIER_WEIGHTS.values()) +# variations like 704 or 2156-pixel encodes. The lowest tier is a catch-all +# floor for undersized output. +_TIER_MIN_DIM_THRESHOLDS: Dict[str, List[Tuple[str, int]]] = { + VIDEO_MODALITY: [ + ("1080p", 1000), # nominal 1080; also absorbs 4K output (no 4K tier yet) + ("720p", 620), # nominal 720 + ("480p", 0), + ], + IMAGE_MODALITY: [ + ("4K", 3500), # nominal 3840/4096 + ("2K", 1900), # nominal 2048 + ("1K", 0), # nominal 1024; catch-all for smaller output + ], +} + + +def sample_challenge_tier(modality: str = VIDEO_MODALITY, rng: random.Random = random) -> str: + """Sample a requested resolution tier from the modality's weight distribution.""" + weights_table = CHALLENGE_TIER_WEIGHTS[modality] + tiers = list(weights_table.keys()) + weights = list(weights_table.values()) return rng.choices(tiers, weights=weights, k=1)[0] -def normalize_tier(tier: Optional[str]) -> Optional[str]: +def normalize_tier(tier: Optional[str], modality: str = VIDEO_MODALITY) -> Optional[str]: """Map a tier string to its canonical form ("4k" -> "4K"), or None if unknown.""" if not tier: return None - for canonical in TIER_ORDER: + for canonical in TIER_ORDER[modality]: if canonical.lower() == tier.strip().lower(): return canonical return None -def tier_from_resolution(resolution: Optional[Tuple[int, int]]) -> Optional[str]: +def tier_from_resolution( + resolution: Optional[Tuple[int, int]], modality: str = VIDEO_MODALITY +) -> Optional[str]: """Classify observed (width, height) pixel dimensions into a tier.""" if not resolution or len(resolution) != 2: return None @@ -67,7 +98,7 @@ def tier_from_resolution(resolution: Optional[Tuple[int, int]]) -> Optional[str] return None if min_dim <= 0: return None - for tier, threshold in _TIER_MIN_DIM_THRESHOLDS: + for tier, threshold in _TIER_MIN_DIM_THRESHOLDS[modality]: if min_dim >= threshold: return tier return None @@ -76,19 +107,21 @@ def tier_from_resolution(resolution: Optional[Tuple[int, int]]) -> Optional[str] def effective_tier( observed_resolution: Optional[Tuple[int, int]], requested_tier: Optional[str], + modality: str = VIDEO_MODALITY, ) -> Optional[str]: """ - Tier a video generation is priced at: min(observed, requested). + Tier a generation is priced at: min(observed, requested). Returns None when the observed resolution is unknown — pricing then falls back to the baseline, since an unverifiable resolution earns no premium. A missing requested tier (e.g. outcomes recorded before tiered challenges shipped) prices at the observed tier alone. """ - observed = tier_from_resolution(observed_resolution) + observed = tier_from_resolution(observed_resolution, modality) if observed is None: return None - requested = normalize_tier(requested_tier) + requested = normalize_tier(requested_tier, modality) if requested is None: return observed - return observed if _TIER_RANK[observed] <= _TIER_RANK[requested] else requested + rank = _TIER_RANK[modality] + return observed if rank[observed] <= rank[requested] else requested diff --git a/gas/evaluation/rewards.py b/gas/evaluation/rewards.py index 0210c0bb..a462e185 100644 --- a/gas/evaluation/rewards.py +++ b/gas/evaluation/rewards.py @@ -23,13 +23,14 @@ "google/veo-3.1-fast": 0.08, "google/veo-3.1": 0.20, # ByteDance Seedance (C2PA: params.model_name — variant IS exposed) - # Pricing is per-token; figures are 720p-with-audio references (USD/s). + # 720p reference (USD/s); per-tier prices in NAMED_MODEL_TIER_PRICES take + # precedence — these flat values only serve the legacy model-name path. # Covers both OpenRouter (bytedance/*) and Runway (seedance2/seedance2_fast) — # both route through ByteDance's own C2PA signing (sig issuer: Byteplus Pte. Ltd.). # Confirmed via live test: seedance-1-5-pro has NO C2PA on OpenRouter and is # not offered on Runway — only the 2.0 variants are validator-eligible. - "dreamina-seedance-2-0-fast": 0.12, # ~$0.121/s at 720p - "dreamina-seedance-2-0": 0.15, # ~$0.151/s at 720p + "dreamina-seedance-2-0-fast": 0.152, # $0.76 per 5s at 720p (provider quote, 2026-07) + "dreamina-seedance-2-0": 0.40, # $2.00 per 5s at 720p (provider quote, 2026-07) # Runway gen4.5: C2PA manifest present but claimSignature.mismatch — validators # reject all gen4.5 content until Runway fixes their signing infra. # "RunwayML": 0.05, # re-enable when gen4.5 signature is fixed @@ -44,6 +45,11 @@ # Keys mirror OpenRouter pricing_skus: the un-suffixed duration_seconds SKU is # the 1080p default; _720p/_4k suffixes are explicit tiers. Defaults below # mirror live values as of 2026-07; live SKUs are merged on top at module load. +# +# 4K entries are inert for now: "4K" is not a video tier (never requested, and +# 4K output classifies as 1080p — see resolution_tiers.py), so no lookup ever +# reaches them. They are kept because the live SKU merge re-adds them anyway, +# and they become active again the moment 4K is added back to the video tiers. MODEL_TIER_PRICES: Dict[str, Dict[Tuple[str, bool], float]] = { "google/veo-3.1-lite": { ("720p", False): 0.03, ("720p", True): 0.05, @@ -63,6 +69,61 @@ # Models priced via the tier floor when C2PA gives model_name=None. _TIER_FLOOR_FAMILY_PREFIX = "google/veo" +# Per-tier USD/s prices for named models (C2PA exposes model_name). +# OpenRouter publishes no Seedance pricing SKUs, so these come from provider +# quotes (2026-07, per 5s video): seedance-2-0 $0.50 / $2.00 / $2.40 at +# 480p / 720p / 1080p; seedance-2-0-fast $0.35 / $0.76 at 480p / 720p. +# Note the spread is NOT pixel-proportional (full jumps ~4x from 480p to +# 720p but only ~1.2x from 720p to 1080p), which is why these are explicit +# tables rather than a shared scale. Keys are matched against the C2PA +# model_name the same way as GENERATOR_MODEL_PRICES (exact, then substring). +NAMED_MODEL_TIER_PRICES: Dict[str, Dict[str, float]] = { + "dreamina-seedance-2-0-fast": { + "480p": 0.07, # $0.35 / 5s + "720p": 0.152, # $0.76 / 5s + }, + "dreamina-seedance-2-0": { + "480p": 0.10, # $0.50 / 5s + "720p": 0.40, # $2.00 / 5s + "1080p": 0.48, # $2.40 / 5s + }, +} + +# Fallback resolution scaling for named models with no NAMED_MODEL_TIER_PRICES +# table: their flat 720p reference is scaled by the delivered tier's pixel +# ratio (480p is ~0.44x the 720p pixel count, 1080p ~2.25x). +NAMED_MODEL_TIER_SCALE: Dict[str, float] = { + "480p": 0.44, + "720p": 1.0, + "1080p": 2.25, +} + + +def _get_named_model_tier_table(model_name: str) -> Optional[Dict[str, float]]: + """Per-tier price table for a C2PA model name (exact, then substring match).""" + lower = model_name.lower() + for key, table in NAMED_MODEL_TIER_PRICES.items(): + if key.lower() == lower: + return table + for key, table in NAMED_MODEL_TIER_PRICES.items(): + if key.lower() in lower: + return table + return None + +# Relative per-image prices by resolution tier (1K is the image baseline). +# Image C2PA manifests rarely expose a usable model variant and image APIs +# price per image (often tiered by output size), so images are priced purely +# by delivered resolution tier: min(observed, requested), same as video. +# Ratios approximate the resolution-tier spreads of the trusted image APIs +# (e.g. GPT Image 2 spans ~3x from 1K to 4K); tune here as providers change. +IMAGE_TIER_PRICES: Dict[str, float] = { + "1K": 1.0, + "2K": 2.0, + "4K": 4.0, +} + +_IMAGE_BASELINE_PRICE: float = min(IMAGE_TIER_PRICES.values()) + # Cheapest model price — all multipliers are relative to this. _GENERATOR_BASELINE_PRICE: float = min(GENERATOR_MODEL_PRICES.values()) @@ -181,21 +242,39 @@ def _compute_average_model_multiplier(model_names: list[str]) -> float: def _get_video_generation_price(generation: Dict[str, Any]) -> float: """USD/s price for one verified video generation. - Named models (Seedance et al. expose model_name via C2PA) use their flat - price. C2PA-blind generations (model_name=None — the Veo family) are - priced at the cheapest Veo variant able to produce the video's - (resolution tier, audio) combination, where the tier is - min(observed, requested) so overshooting a challenge request never pays. - An unknown observed resolution earns only the baseline. - """ - model_name = generation.get("model_name") - if model_name: - return _get_model_price(model_name) + In both branches the tier is min(observed, requested) so overshooting a + challenge request never pays, and an unknown observed resolution earns + only the baseline — no premium without a verifiable resolution. + Named models (Seedance et al. expose model_name via C2PA, and are + per-token priced upstream) use their 720p reference price scaled by the + delivered tier's pixel ratio (NAMED_MODEL_TIER_SCALE). C2PA-blind + generations (model_name=None — the Veo family) are priced at the cheapest + Veo variant able to produce the video's (tier, audio) combination. + """ tier = effective_tier( generation.get("observed_resolution"), generation.get("requested_resolution"), ) + model_name = generation.get("model_name") + if model_name: + if tier is None: + return _GENERATOR_BASELINE_PRICE + table = _get_named_model_tier_table(model_name) + if table and tier in table: + price = table[tier] + elif table: + # Tier absent from the table (e.g. a fast variant somehow observed + # at 1080p): pay the highest tier the table does price — the model + # can't officially produce more, so no extrapolated premium. + price = max(table.values()) + else: + price = _get_model_price(model_name) * NAMED_MODEL_TIER_SCALE.get(tier, 1.0) + # Baseline is the floor: a named model unknown to the price table + # resolves to the baseline price, and tier down-scaling must not push + # it (or any cheap model) below what an unpriceable generation earns. + return max(price, _GENERATOR_BASELINE_PRICE) + if tier is None: return _GENERATOR_BASELINE_PRICE @@ -226,6 +305,36 @@ def _compute_video_generation_multiplier(generations: List[Dict[str, Any]]) -> f return total / len(generations) +def _get_image_generation_price(generation: Dict[str, Any]) -> float: + """Relative price for one verified image generation, by delivered tier. + + Tier is min(observed, requested) so overshooting a challenge request never + pays more. An unknown observed resolution earns only the 1K baseline. + """ + tier = effective_tier( + generation.get("observed_resolution"), + generation.get("requested_resolution"), + modality="image", + ) + if tier is None: + return _IMAGE_BASELINE_PRICE + return IMAGE_TIER_PRICES.get(tier, _IMAGE_BASELINE_PRICE) + + +def _compute_image_generation_multiplier(generations: List[Dict[str, Any]]) -> float: + """Average resolution-tier multiplier across a miner's verified images. + + Same sqrt taper as video: a tier 4x the baseline price earns 2x, not 4x. + """ + if not generations: + return 1.0 + total = 0.0 + for generation in generations: + price = _get_image_generation_price(generation) + total += math.sqrt(price / _IMAGE_BASELINE_PRICE) + return total / len(generations) + + def get_generator_base_rewards(verification_stats): """ Compute base rewards for generators based on their verification pass rates, @@ -250,6 +359,7 @@ def get_generator_base_rewards(verification_stats): "video_failed": int, "video_pass_rate": float, "video_model_names": list[str], + "image_generations": list[dict], # per-generation resolution tiers "video_generations": list[dict], # per-generation model/resolution/audio "media_ids": List[str] } @@ -276,9 +386,16 @@ def get_generator_base_rewards(verification_stats): image_pass_rate = stats.get("image_pass_rate", 0.0) image_volume = min(image_verified, 10) + max(0.0, math.log2(max(1, image_verified - 9))) image_base = image_pass_rate * image_volume - image_model_mult = _compute_average_model_multiplier( - stats.get("image_model_names", []) - ) + # Prefer per-generation resolution-tier pricing; fall back to model + # names for stats produced before tiered pricing (all baseline today, + # since no image models carry a flat price). + image_generations = stats.get("image_generations") + if image_generations: + image_model_mult = _compute_image_generation_multiplier(image_generations) + else: + image_model_mult = _compute_average_model_multiplier( + stats.get("image_model_names", []) + ) image_base *= image_model_mult # --- Video modality --- From a1ddf98cfbd4466125552032aab0fcc9ad489c1e Mon Sep 17 00:00:00 2001 From: kenobijon Date: Mon, 20 Jul 2026 19:06:27 -0500 Subject: [PATCH 4/6] Price C2PA-blind Veo as the fast variant, not the lite floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Veo manifests hide the variant, but a Google cert is cryptographic proof of real paid generation — flooring at lite credited every fast/full video at lite rates and capped the whole family at 1.63x. Veo is now priced as the middle variant (_VEO_FLOOR_MODEL = veo-3.1-fast), bounding a lite miner's overcredit at ~2x on price; since challenge volume is demand-limited this doesn't reorder anyone's model choice. Veo also cannot produce sub-720p output (720p is the family minimum), so a 480p-tier challenge previously priced a real 720p Veo generation at the baseline. Tiers below the family minimum now price at the floor variant's cheapest producible combo with the same audio state. --- gas/evaluation/rewards.py | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/gas/evaluation/rewards.py b/gas/evaluation/rewards.py index a462e185..fe1535ac 100644 --- a/gas/evaluation/rewards.py +++ b/gas/evaluation/rewards.py @@ -37,10 +37,10 @@ } # Per-(resolution tier, audio) USD/s prices for models whose C2PA manifests -# hide the variant (the Veo family). A Veo video is priced at the cheapest -# variant able to produce its observed (tier, audio) combination — a price -# floor. Since challenges request a tier and rewards use -# min(observed, requested), overshooting a request never pays more. +# hide the variant (the Veo family). A Veo video is priced as the floor +# variant (_VEO_FLOOR_MODEL) at its observed (tier, audio) combination. +# Since challenges request a tier and rewards use min(observed, requested), +# overshooting a request never pays more. # # Keys mirror OpenRouter pricing_skus: the un-suffixed duration_seconds SKU is # the 1080p default; _720p/_4k suffixes are explicit tiers. Defaults below @@ -69,6 +69,15 @@ # Models priced via the tier floor when C2PA gives model_name=None. _TIER_FLOOR_FAMILY_PREFIX = "google/veo" +# The variant a C2PA-blind Veo generation is priced as. Google's manifests +# hide the variant, so some assumption is unavoidable: flooring at lite +# (cheapest) is maximally arbitrage-safe but credits every real fast/full +# generation at lite rates; flooring at fast (the middle variant) bounds a +# lite miner's overcredit at ~2x on price while Google-certified content — +# cryptographic proof of real, paid generation — earns a fair mid-family +# rate. Volume is demand-limited, so this doesn't reorder model choice. +_VEO_FLOOR_MODEL = "google/veo-3.1-fast" + # Per-tier USD/s prices for named models (C2PA exposes model_name). # OpenRouter publishes no Seedance pricing SKUs, so these come from provider # quotes (2026-07, per 5s video): seedance-2-0 $0.50 / $2.00 / $2.40 at @@ -246,11 +255,13 @@ def _get_video_generation_price(generation: Dict[str, Any]) -> float: challenge request never pays, and an unknown observed resolution earns only the baseline — no premium without a verifiable resolution. - Named models (Seedance et al. expose model_name via C2PA, and are - per-token priced upstream) use their 720p reference price scaled by the - delivered tier's pixel ratio (NAMED_MODEL_TIER_SCALE). C2PA-blind - generations (model_name=None — the Veo family) are priced at the cheapest - Veo variant able to produce the video's (tier, audio) combination. + Named models (Seedance et al. expose model_name via C2PA) use their + per-tier quoted prices (NAMED_MODEL_TIER_PRICES), falling back to + pixel-ratio scaling of their flat 720p reference. C2PA-blind + generations (model_name=None — the Veo family) are priced as the floor + variant (_VEO_FLOOR_MODEL) at the video's (tier, audio) combination; + tiers below the family's minimum output price at its cheapest + producible combo. """ tier = effective_tier( generation.get("observed_resolution"), @@ -279,14 +290,17 @@ def _get_video_generation_price(generation: Dict[str, Any]) -> float: return _GENERATOR_BASELINE_PRICE has_audio = bool(generation.get("has_audio")) - candidates = [ - table[(tier, has_audio)] - for model, table in MODEL_TIER_PRICES.items() - if model.startswith(_TIER_FLOOR_FAMILY_PREFIX) and (tier, has_audio) in table - ] - # No variant offers this (tier, audio) — e.g. sub-720p output — so no - # premium can be justified. - return min(candidates) if candidates else _GENERATOR_BASELINE_PRICE + floor_table = MODEL_TIER_PRICES.get(_VEO_FLOOR_MODEL, {}) + price = floor_table.get((tier, has_audio)) + if price is None: + # Tier below the family's minimum output (Veo can't produce sub-720p, + # so a 480p-tier request still costs the miner a real 720p generation): + # price at the cheapest combo the floor variant CAN produce with the + # same audio state, not the baseline. + same_audio = [p for (t, a), p in floor_table.items() if a == has_audio] + candidates = same_audio or list(floor_table.values()) + price = min(candidates) if candidates else _GENERATOR_BASELINE_PRICE + return max(price, _GENERATOR_BASELINE_PRICE) def _compute_video_generation_multiplier(generations: List[Dict[str, Any]]) -> float: From e804b4aa4591fc42b8556faba6e2e2b2c74e501e Mon Sep 17 00:00:00 2001 From: kenobijon Date: Mon, 20 Jul 2026 19:53:17 -0500 Subject: [PATCH 5/6] Match Veo's sub-minimum 480p tier to Seedance fast's 480p rate A 480p-tier Veo submission is a real 720p generation (720p is the family minimum), but paying it 720p rates made the bottom tier family-dependent. Price it at Seedance fast's 480p rate so 480p challenges pay 1.53x regardless of which family serves them. --- gas/evaluation/rewards.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gas/evaluation/rewards.py b/gas/evaluation/rewards.py index fe1535ac..1be1d32f 100644 --- a/gas/evaluation/rewards.py +++ b/gas/evaluation/rewards.py @@ -294,12 +294,10 @@ def _get_video_generation_price(generation: Dict[str, Any]) -> float: price = floor_table.get((tier, has_audio)) if price is None: # Tier below the family's minimum output (Veo can't produce sub-720p, - # so a 480p-tier request still costs the miner a real 720p generation): - # price at the cheapest combo the floor variant CAN produce with the - # same audio state, not the baseline. - same_audio = [p for (t, a), p in floor_table.items() if a == has_audio] - candidates = same_audio or list(floor_table.values()) - price = min(candidates) if candidates else _GENERATOR_BASELINE_PRICE + # so a 480p-tier request still costs the miner a real 720p + # generation): match Seedance fast's 480p rate so the bottom tier + # pays the same regardless of family. + price = NAMED_MODEL_TIER_PRICES["dreamina-seedance-2-0-fast"]["480p"] return max(price, _GENERATOR_BASELINE_PRICE) From a7ebdb4f0e4ef25eaf0d79889669e526a31d15e2 Mon Sep 17 00:00:00 2001 From: dylan Date: Tue, 21 Jul 2026 15:25:21 +0000 Subject: [PATCH 6/6] bump version --- VERSION | 2 +- gas/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index c01c4133..fa423416 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.9.3 +4.9.4 diff --git a/gas/__init__.py b/gas/__init__.py index c1da49ed..1c13c99b 100644 --- a/gas/__init__.py +++ b/gas/__init__.py @@ -1,4 +1,4 @@ -__version__ = "4.9.3" +__version__ = "4.9.4" version_split = __version__.split(".") __spec_version__ = (