diff --git a/model_cost/model_cost.json b/model_cost/model_cost.json index bf12991d..5fdc0dd9 100644 --- a/model_cost/model_cost.json +++ b/model_cost/model_cost.json @@ -96,12 +96,12 @@ "output_token_price_per_million": 4.94 }, "deepseek/deepseek-chat": { - "input_token_price_per_million": 0.2574, - "output_token_price_per_million": 1.0287 + "input_token_price_per_million": 0.286, + "output_token_price_per_million": 1.143 }, "deepseek-chat": { - "input_token_price_per_million": 0.2574, - "output_token_price_per_million": 1.0287 + "input_token_price_per_million": 0.286, + "output_token_price_per_million": 1.143 }, "deepseek/deepseek-reasoner": { "input_token_price_per_million": 0.28, @@ -112,40 +112,40 @@ "output_token_price_per_million": 0.42 }, "deepseek/deepseek-v3.2": { - "input_token_price_per_million": 0.26, - "output_token_price_per_million": 0.38 + "input_token_price_per_million": 0.295, + "output_token_price_per_million": 0.88 }, "deepseek-v3.2": { - "input_token_price_per_million": 0.26, - "output_token_price_per_million": 0.38 + "input_token_price_per_million": 0.295, + "output_token_price_per_million": 0.88 }, "deepseek/deepseek-v4-flash": { - "input_token_price_per_million": 0.091, - "output_token_price_per_million": 0.182 + "input_token_price_per_million": 0.14, + "output_token_price_per_million": 0.28 }, "deepseek/deepseek-v4-flash-0731": { - "input_token_price_per_million": 0.119, - "output_token_price_per_million": 0.238 + "input_token_price_per_million": 0.176, + "output_token_price_per_million": 0.528 }, "deepseek-ai/DeepSeek-V4-Flash-0731": { - "input_token_price_per_million": 0.119, - "output_token_price_per_million": 0.238 + "input_token_price_per_million": 0.176, + "output_token_price_per_million": 0.528 }, "deepseek/deepseek-v4-pro": { "input_token_price_per_million": 1.6, "output_token_price_per_million": 3.2 }, "deepseek/deepseek-v4.1-flash": { - "input_token_price_per_million": 0.045, - "output_token_price_per_million": 0.5 + "input_token_price_per_million": 0.3, + "output_token_price_per_million": 1.2 }, "deepseek/deepseek-v4.1-flash@high": { - "input_token_price_per_million": 0.045, - "output_token_price_per_million": 0.5 + "input_token_price_per_million": 0.3, + "output_token_price_per_million": 1.2 }, "deepseek/deepseek-v4.1-flash@low": { - "input_token_price_per_million": 0.045, - "output_token_price_per_million": 0.5 + "input_token_price_per_million": 0.3, + "output_token_price_per_million": 1.2 }, "gemini-2.0-flash-001": { "input_token_price_per_million": 0.1, @@ -220,23 +220,23 @@ "output_token_price_per_million": 2.5 }, "google/gemini-3.7-flash": { - "input_token_price_per_million": 0.75, - "output_token_price_per_million": 3.75 + "input_token_price_per_million": 1.5, + "output_token_price_per_million": 7.5 }, "google/gemini-3.8-flash": { - "input_token_price_per_million": 0.825, - "output_token_price_per_million": 4.125 + "input_token_price_per_million": 1.5, + "output_token_price_per_million": 7.5 }, "gemini-3.8-flash": { - "input_token_price_per_million": 0.825, - "output_token_price_per_million": 4.125 + "input_token_price_per_million": 1.5, + "output_token_price_per_million": 7.5 }, "google/gemma-3n-e4b-it": { "input_token_price_per_million": 0.02, "output_token_price_per_million": 0.04 }, "google/gemma-4-31b-it": { - "input_token_price_per_million": 0.2, + "input_token_price_per_million": 0.15, "output_token_price_per_million": 0.4 }, "gpt-5-chat": { @@ -252,24 +252,24 @@ "output_token_price_per_million": 27.5 }, "inclusionai/ling-3.0-flash": { - "input_token_price_per_million": 0.021, - "output_token_price_per_million": 0.063 + "input_token_price_per_million": 0.06, + "output_token_price_per_million": 0.18 }, "lingdt-3.0-flash": { - "input_token_price_per_million": 0.021, - "output_token_price_per_million": 0.063 + "input_token_price_per_million": 0.06, + "output_token_price_per_million": 0.18 }, "llama-4-maverick-17b-128e-instruct-fp8": { "input_token_price_per_million": 0.25, "output_token_price_per_million": 1 }, "meta-llama/llama-3.3-70b-instruct": { - "input_token_price_per_million": 0.1, - "output_token_price_per_million": 0.32 + "input_token_price_per_million": 0.4415, + "output_token_price_per_million": 0.715 }, "meta-llama_llama-3.3-70b-instruct": { - "input_token_price_per_million": 0.1, - "output_token_price_per_million": 0.32 + "input_token_price_per_million": 0.4415, + "output_token_price_per_million": 0.715 }, "meta-llama_llama-3.1-405b-instruct": { "input_token_price_per_million": 3.5, @@ -316,19 +316,19 @@ "output_token_price_per_million": 0.22 }, "moonshotai/kimi-k2.5": { - "input_token_price_per_million": 0.5, - "output_token_price_per_million": 2.5 + "input_token_price_per_million": 0.6, + "output_token_price_per_million": 3 }, "moonshotai/kimi-k2.6": { - "input_token_price_per_million": 0.65, - "output_token_price_per_million": 3.41 + "input_token_price_per_million": 0.95, + "output_token_price_per_million": 4 }, "moonshotai/Kimi-K2.6": { - "input_token_price_per_million": 0.65, - "output_token_price_per_million": 3.41 + "input_token_price_per_million": 0.95, + "output_token_price_per_million": 4 }, "moonshotai/kimi-k3": { - "input_token_price_per_million": 0.192, + "input_token_price_per_million": 3, "output_token_price_per_million": 15 }, "nvidia/nemotron-3-nano-30b-a3b": { @@ -364,12 +364,12 @@ "output_token_price_per_million": 1.6 }, "openai/gpt-4.1-nano": { - "input_token_price_per_million": 0.1, - "output_token_price_per_million": 0.4 + "input_token_price_per_million": 0.105, + "output_token_price_per_million": 0.42 }, "gpt-4.1-nano": { - "input_token_price_per_million": 0.1, - "output_token_price_per_million": 0.4 + "input_token_price_per_million": 0.105, + "output_token_price_per_million": 0.42 }, "openai/gpt-4o": { "input_token_price_per_million": 2.5, @@ -520,52 +520,52 @@ "output_token_price_per_million": 0.6 }, "openai/gpt-oss-20b": { - "input_token_price_per_million": 0.018, - "output_token_price_per_million": 0.09 + "input_token_price_per_million": 0.04, + "output_token_price_per_million": 0.15 }, "gpt-oss-20b": { - "input_token_price_per_million": 0.018, - "output_token_price_per_million": 0.09 + "input_token_price_per_million": 0.04, + "output_token_price_per_million": 0.15 }, "qwen/qwen3-235b-a22b-2507": { - "input_token_price_per_million": 0.09, - "output_token_price_per_million": 0.55 + "input_token_price_per_million": 0.145, + "output_token_price_per_million": 0.775 }, "alibaba/qwen3-235b-a22b-instruct-2507": { - "input_token_price_per_million": 0.09, - "output_token_price_per_million": 0.55 + "input_token_price_per_million": 0.145, + "output_token_price_per_million": 0.775 }, "qwen3-235b-a22b-instruct-2507": { - "input_token_price_per_million": 0.09, - "output_token_price_per_million": 0.55 + "input_token_price_per_million": 0.145, + "output_token_price_per_million": 0.775 }, "qwen_qwen3-235b-a22b-2507": { - "input_token_price_per_million": 0.09, - "output_token_price_per_million": 0.55 + "input_token_price_per_million": 0.145, + "output_token_price_per_million": 0.775 }, "qwen/qwen3-30b-a3b-instruct-2507": { - "input_token_price_per_million": 0.1, + "input_token_price_per_million": 0.09, "output_token_price_per_million": 0.3 }, "alibaba/qwen3-30b-a3b-instruct-2507": { - "input_token_price_per_million": 0.1, + "input_token_price_per_million": 0.09, "output_token_price_per_million": 0.3 }, "qwen3-30b-a3b-instruct-2507": { - "input_token_price_per_million": 0.1, + "input_token_price_per_million": 0.09, "output_token_price_per_million": 0.3 }, "qwen/qwen3-coder": { - "input_token_price_per_million": 0.3, - "output_token_price_per_million": 1 + "input_token_price_per_million": 0.35, + "output_token_price_per_million": 1.5 }, "qwen_qwen3-coder": { - "input_token_price_per_million": 0.3, - "output_token_price_per_million": 1 + "input_token_price_per_million": 0.35, + "output_token_price_per_million": 1.5 }, "qwen/qwen3-coder-30b-a3b-instruct": { - "input_token_price_per_million": 0.07, - "output_token_price_per_million": 0.28 + "input_token_price_per_million": 0.11, + "output_token_price_per_million": 0.44 }, "qwen/qwen3-coder-next": { "input_token_price_per_million": 0.12, @@ -576,7 +576,7 @@ "output_token_price_per_million": 0.8 }, "qwen/qwen3-next-80b-a3b-instruct": { - "input_token_price_per_million": 0.09, + "input_token_price_per_million": 0.095, "output_token_price_per_million": 1.1 }, "qwen/qwen3-vl-235b-a22b-instruct": { @@ -588,8 +588,8 @@ "output_token_price_per_million": 1.9 }, "qwen/qwen3.5-9b": { - "input_token_price_per_million": 0.17, - "output_token_price_per_million": 0.25 + "input_token_price_per_million": 0.1, + "output_token_price_per_million": 0.15 }, "qwen/qwen3.5-flash-02-23": { "input_token_price_per_million": 0.065, @@ -624,8 +624,8 @@ "output_token_price_per_million": 0.6 }, "upstage/solar-pro4": { - "input_token_price_per_million": 0.15, - "output_token_price_per_million": 0.6 + "input_token_price_per_million": 0.3, + "output_token_price_per_million": 1.2 }, "x-ai/grok-4.1-fast": { "input_token_price_per_million": 0.2, @@ -660,20 +660,20 @@ "output_token_price_per_million": 0.3 }, "z-ai/glm-4.5-air": { - "input_token_price_per_million": 0.13, - "output_token_price_per_million": 0.85 + "input_token_price_per_million": 0.2, + "output_token_price_per_million": 1.1 }, "glm-4.5-air": { - "input_token_price_per_million": 0.13, - "output_token_price_per_million": 0.85 + "input_token_price_per_million": 0.2, + "output_token_price_per_million": 1.1 }, "z-ai/glm-4.6": { - "input_token_price_per_million": 0.5, - "output_token_price_per_million": 2 + "input_token_price_per_million": 0.6, + "output_token_price_per_million": 2.2 }, "glm-4.6": { - "input_token_price_per_million": 0.5, - "output_token_price_per_million": 2 + "input_token_price_per_million": 0.6, + "output_token_price_per_million": 2.2 }, "z-ai/glm-4.7": { "input_token_price_per_million": 0.6, @@ -688,27 +688,27 @@ "output_token_price_per_million": 3.2 }, "z-ai/glm-5.3": { - "input_token_price_per_million": 0.0699, - "output_token_price_per_million": 1.4 + "input_token_price_per_million": 1.4, + "output_token_price_per_million": 4.4 }, "z-ai/glm-5.3@high": { - "input_token_price_per_million": 0.0699, - "output_token_price_per_million": 1.4 + "input_token_price_per_million": 1.4, + "output_token_price_per_million": 4.4 }, "z-ai/glm-5.3@low": { - "input_token_price_per_million": 0.0699, - "output_token_price_per_million": 1.4 + "input_token_price_per_million": 1.4, + "output_token_price_per_million": 4.4 }, "z-ai/glm-5.3-flash": { - "input_token_price_per_million": 0.0149, - "output_token_price_per_million": 0.1543 + "input_token_price_per_million": 0.15, + "output_token_price_per_million": 0.5 }, "glm-5.3-flash": { - "input_token_price_per_million": 0.0149, - "output_token_price_per_million": 0.1543 + "input_token_price_per_million": 0.15, + "output_token_price_per_million": 0.5 }, "z-ai/glm-5.3-flash@default": { - "input_token_price_per_million": 0.0149, - "output_token_price_per_million": 0.1543 + "input_token_price_per_million": 0.15, + "output_token_price_per_million": 0.5 } } diff --git a/model_cost/model_profiles.yaml b/model_cost/model_profiles.yaml index 4d280dce..f4c45140 100644 --- a/model_cost/model_profiles.yaml +++ b/model_cost/model_profiles.yaml @@ -92,7 +92,7 @@ models: as_of: "2026-09-27" source: null "deepseek/deepseek-chat": - price: {input: 0.2574, output: 1.0287} + price: {input: 0.286, output: 1.143} aliases: ["deepseek-chat"] as_of: "2026-10-31" source: "https://openrouter.ai/deepseek/deepseek-chat" @@ -104,16 +104,16 @@ models: note: "DeepSeek's API name; the model behind it changed over time, so it is not synced." sync: false "deepseek/deepseek-v3.2": - price: {input: 0.26, output: 0.38} + price: {input: 0.295, output: 0.88} aliases: ["deepseek-v3.2"] - as_of: "2026-09-27" - source: null + as_of: "2026-10-31" + source: "https://openrouter.ai/deepseek/deepseek-v3.2" "deepseek/deepseek-v4-flash": - price: {input: 0.091, output: 0.182} + price: {input: 0.14, output: 0.28} as_of: "2026-10-31" source: "https://openrouter.ai/deepseek/deepseek-v4-flash" "deepseek/deepseek-v4-flash-0731": - price: {input: 0.119, output: 0.238} + price: {input: 0.176, output: 0.528} aliases: ["deepseek-ai/DeepSeek-V4-Flash-0731"] as_of: "2026-10-31" source: "https://openrouter.ai/deepseek/deepseek-v4-flash-0731" @@ -122,7 +122,7 @@ models: as_of: "2026-10-31" source: "https://openrouter.ai/deepseek/deepseek-v4-pro" "deepseek/deepseek-v4.1-flash": - price: {input: 0.045, output: 0.5} + price: {input: 0.3, output: 1.2} aliases: ["deepseek/deepseek-v4.1-flash@high", "deepseek/deepseek-v4.1-flash@low"] as_of: "2026-10-31" source: "https://openrouter.ai/deepseek/deepseek-v4.1-flash" @@ -183,11 +183,11 @@ models: as_of: null source: null "google/gemini-3.7-flash": - price: {input: 0.75, output: 3.75} - as_of: null - source: null + price: {input: 1.5, output: 7.5} + as_of: "2026-10-31" + source: "https://openrouter.ai/google/gemini-3.7-flash" "google/gemini-3.8-flash": - price: {input: 0.825, output: 4.125} + price: {input: 1.5, output: 7.5} aliases: ["gemini-3.8-flash"] as_of: "2026-10-31" source: "https://openrouter.ai/google/gemini-3.8-flash" @@ -196,7 +196,7 @@ models: as_of: "2026-09-27" source: null "google/gemma-4-31b-it": - price: {input: 0.2, output: 0.4} + price: {input: 0.15, output: 0.4} as_of: "2026-10-31" source: "https://openrouter.ai/google/gemma-4-31b-it" "gpt-5-chat": @@ -212,7 +212,7 @@ models: as_of: "2026-09-27" source: null "inclusionai/ling-3.0-flash": - price: {input: 0.021, output: 0.063} + price: {input: 0.06, output: 0.18} aliases: ["lingdt-3.0-flash"] as_of: "2026-10-31" source: "https://openrouter.ai/inclusionai/ling-3.0-flash" @@ -222,10 +222,10 @@ models: as_of: "2026-09-27" source: null "meta-llama/llama-3.3-70b-instruct": - price: {input: 0.1, output: 0.32} + price: {input: 0.4415, output: 0.715} aliases: ["meta-llama_llama-3.3-70b-instruct"] - as_of: "2026-09-27" - source: null + as_of: "2026-10-31" + source: "https://openrouter.ai/meta-llama/llama-3.3-70b-instruct" "meta-llama_llama-3.1-405b-instruct": price: {input: 3.5, output: 3.5} as_of: "2026-09-27" @@ -261,16 +261,16 @@ models: as_of: "2026-09-27" source: null "moonshotai/kimi-k2.5": - price: {input: 0.5, output: 2.5} + price: {input: 0.6, output: 3} as_of: "2026-10-31" source: "https://openrouter.ai/moonshotai/kimi-k2.5" "moonshotai/kimi-k2.6": - price: {input: 0.65, output: 3.41} + price: {input: 0.95, output: 4} aliases: ["moonshotai/Kimi-K2.6"] as_of: "2026-10-31" source: "https://openrouter.ai/moonshotai/kimi-k2.6" "moonshotai/kimi-k3": - price: {input: 0.192, output: 15} + price: {input: 3, output: 15} as_of: "2026-10-31" source: "https://openrouter.ai/moonshotai/kimi-k3" "nvidia/nemotron-3-nano-30b-a3b": @@ -295,10 +295,10 @@ models: as_of: "2026-09-27" source: null "openai/gpt-4.1-nano": - price: {input: 0.1, output: 0.4} + price: {input: 0.105, output: 0.42} aliases: ["gpt-4.1-nano"] - as_of: "2026-09-27" - source: null + as_of: "2026-10-31" + source: "https://openrouter.ai/openai/gpt-4.1-nano" "openai/gpt-4o": price: {input: 2.5, output: 10} aliases: ["gpt-4o"] @@ -390,29 +390,29 @@ models: as_of: "2026-09-27" source: null "openai/gpt-oss-20b": - price: {input: 0.018, output: 0.09} + price: {input: 0.04, output: 0.15} aliases: ["gpt-oss-20b"] as_of: "2026-10-31" source: "https://openrouter.ai/openai/gpt-oss-20b" "qwen/qwen3-235b-a22b-2507": - price: {input: 0.09, output: 0.55} + price: {input: 0.145, output: 0.775} aliases: ["alibaba/qwen3-235b-a22b-instruct-2507", "qwen3-235b-a22b-instruct-2507", "qwen_qwen3-235b-a22b-2507"] as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-235b-a22b-2507" note: "Unified spellings; previous prices: qwen3-235b-a22b-instruct-2507 0.5/2. alibaba/qwen3-235b-a22b-instruct-2507: same model, Alibaba's spelling." "qwen/qwen3-30b-a3b-instruct-2507": - price: {input: 0.1, output: 0.3} + price: {input: 0.09, output: 0.3} aliases: ["alibaba/qwen3-30b-a3b-instruct-2507", "qwen3-30b-a3b-instruct-2507"] as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-30b-a3b-instruct-2507" note: "Unified spellings; previous prices: qwen/qwen3-30b-a3b-instruct-2507 0.08/0.33." "qwen/qwen3-coder": - price: {input: 0.3, output: 1} + price: {input: 0.35, output: 1.5} aliases: ["qwen_qwen3-coder"] as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-coder" "qwen/qwen3-coder-30b-a3b-instruct": - price: {input: 0.07, output: 0.28} + price: {input: 0.11, output: 0.44} as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-coder-30b-a3b-instruct" "qwen/qwen3-coder-next": @@ -421,16 +421,16 @@ models: as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-coder-next" "qwen/qwen3-next-80b-a3b-instruct": - price: {input: 0.09, output: 1.1} - as_of: "2026-09-27" - source: null + price: {input: 0.095, output: 1.1} + as_of: "2026-10-31" + source: "https://openrouter.ai/qwen/qwen3-next-80b-a3b-instruct" "qwen/qwen3-vl-235b-a22b-instruct": price: {input: 0.21, output: 1.9} aliases: ["qwen_qwen3-vl-235b-a22b-instruct"] as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3-vl-235b-a22b-instruct" "qwen/qwen3.5-9b": - price: {input: 0.17, output: 0.25} + price: {input: 0.1, output: 0.15} as_of: "2026-10-31" source: "https://openrouter.ai/qwen/qwen3.5-9b" "qwen/qwen3.5-flash-02-23": @@ -468,7 +468,7 @@ models: as_of: null source: null "upstage/solar-pro4": - price: {input: 0.15, output: 0.6} + price: {input: 0.3, output: 1.2} as_of: "2026-10-31" source: "https://openrouter.ai/upstage/solar-pro4" "x-ai/grok-4.1-fast": @@ -496,12 +496,12 @@ models: as_of: "2026-09-27" source: null "z-ai/glm-4.5-air": - price: {input: 0.13, output: 0.85} + price: {input: 0.2, output: 1.1} aliases: ["glm-4.5-air"] as_of: "2026-10-31" source: "https://openrouter.ai/z-ai/glm-4.5-air" "z-ai/glm-4.6": - price: {input: 0.5, output: 2} + price: {input: 0.6, output: 2.2} aliases: ["glm-4.6"] as_of: "2026-10-31" source: "https://openrouter.ai/z-ai/glm-4.6" @@ -515,12 +515,12 @@ models: as_of: "2026-09-27" source: null "z-ai/glm-5.3": - price: {input: 0.0699, output: 1.4} + price: {input: 1.4, output: 4.4} aliases: ["z-ai/glm-5.3@high", "z-ai/glm-5.3@low"] as_of: "2026-10-31" source: "https://openrouter.ai/z-ai/glm-5.3" "z-ai/glm-5.3-flash": - price: {input: 0.0149, output: 0.1543} + price: {input: 0.15, output: 0.5} aliases: ["glm-5.3-flash", "z-ai/glm-5.3-flash@default"] as_of: "2026-10-31" source: "https://openrouter.ai/z-ai/glm-5.3-flash" diff --git a/model_cost/price_snapshots/2026-10-31.json b/model_cost/price_snapshots/2026-10-31.json index 3a438eed..db56729a 100644 --- a/model_cost/price_snapshots/2026-10-31.json +++ b/model_cost/price_snapshots/2026-10-31.json @@ -1,306 +1,457 @@ { "date": "2026-10-31", - "source": "https://openrouter.ai/api/v1/models", + "source": "https://openrouter.ai/api/v1/models//endpoints", + "rule": "standard price: the lab's own standard-tier price, else the median over hosts (no 4-bit); promotional discounts undone", "prices": { "anthropic/claude-haiku-4.5": { "input": 1.0, - "output": 5.0 + "output": 5.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-haiku-5.5": { "input": 0.1, - "output": 0.5 + "output": 0.5, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-opus-4.1": { "input": 15.0, - "output": 75.0 + "output": 75.0, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "anthropic/claude-opus-4.6": { "input": 5.0, - "output": 25.0 + "output": 25.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-opus-4.7": { "input": 5.0, - "output": 25.0 + "output": 25.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-opus-5.5": { "input": 4.0, - "output": 20.0 + "output": 20.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-sonnet-4.5": { "input": 3.0, - "output": 15.0 + "output": 15.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "anthropic/claude-sonnet-5.5": { "input": 2.0, - "output": 10.0 + "output": 10.0, + "basis": "Anthropic (the lab's own price)", + "discount_removed": 0.0 }, "deepseek/deepseek-chat": { - "input": 0.2574, - "output": 1.0287 + "input": 0.286, + "output": 1.143, + "basis": "median of 1 host(s)", + "discount_removed": 0.1 }, "deepseek/deepseek-v3.2": { - "input": 0.26, - "output": 0.38 + "input": 0.295, + "output": 0.88, + "basis": "median of 8 host(s)", + "discount_removed": 0.28 }, "deepseek/deepseek-v4-flash": { - "input": 0.091, - "output": 0.182 + "input": 0.14, + "output": 0.28, + "basis": "median of 13 host(s)", + "discount_removed": 0.715 }, "deepseek/deepseek-v4-flash-0731": { - "input": 0.119, - "output": 0.238 + "input": 0.176, + "output": 0.528, + "basis": "median of 19 host(s)", + "discount_removed": 0.899 }, "deepseek/deepseek-v4-pro": { "input": 1.6, - "output": 3.2 + "output": 3.2, + "basis": "median of 13 host(s)", + "discount_removed": 0.6923 }, "deepseek/deepseek-v4.1-flash": { - "input": 0.045, - "output": 0.5 + "input": 0.3, + "output": 1.2, + "basis": "median of 30 host(s)", + "discount_removed": 0.851 }, "google/gemini-2.5-flash": { "input": 0.3, - "output": 2.5 + "output": 2.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-2.5-flash-lite": { "input": 0.1, - "output": 0.4 + "output": 0.4, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-2.5-pro": { "input": 1.25, - "output": 10.0 + "output": 10.0, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3-flash-preview": { "input": 0.5, - "output": 3.0 + "output": 3.0, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3.1-flash-lite": { "input": 0.25, - "output": 1.5 + "output": 1.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3.1-flash-lite-preview": { "input": 0.25, - "output": 1.5 + "output": 1.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3.1-pro-preview": { "input": 2.0, - "output": 12.0 + "output": 12.0, + "basis": "Google (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3.5-flash-lite": { "input": 0.3, - "output": 2.5 + "output": 2.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.0 }, "google/gemini-3.7-flash": { - "input": 0.75, - "output": 3.75 + "input": 1.5, + "output": 7.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.5 }, "google/gemini-3.8-flash": { - "input": 0.825, - "output": 4.125 + "input": 1.5, + "output": 7.5, + "basis": "Google AI Studio (the lab's own price)", + "discount_removed": 0.5 }, "google/gemma-4-31b-it": { - "input": 0.2, - "output": 0.4 + "input": 0.15, + "output": 0.4, + "basis": "median of 7 host(s)", + "discount_removed": 0.05 }, "inclusionai/ling-3.0-flash": { - "input": 0.021, - "output": 0.063 + "input": 0.06, + "output": 0.18, + "basis": "median of 1 host(s)", + "discount_removed": 0.65 }, "meta-llama/llama-3.3-70b-instruct": { - "input": 0.1, - "output": 0.32 + "input": 0.4415, + "output": 0.715, + "basis": "median of 10 host(s)", + "discount_removed": 0.25 }, "minimax/minimax-m3": { "input": 0.3, - "output": 1.2 + "output": 1.2, + "basis": "median of 9 host(s)", + "discount_removed": 0.5 }, "mistralai/ministral-3b-2512": { "input": 0.1, - "output": 0.1 + "output": 0.1, + "basis": "Mistral (the lab's own price)", + "discount_removed": 0.0 }, "mistralai/ministral-8b-2512": { "input": 0.15, - "output": 0.15 + "output": 0.15, + "basis": "Mistral (the lab's own price)", + "discount_removed": 0.0 }, "moonshotai/kimi-k2.5": { - "input": 0.5, - "output": 2.5 + "input": 0.6, + "output": 3.0, + "basis": "median of 3 host(s)", + "discount_removed": 0.05 }, "moonshotai/kimi-k2.6": { - "input": 0.65, - "output": 3.41 + "input": 0.95, + "output": 4.0, + "basis": "Moonshot AI (the lab's own price)", + "discount_removed": 0.0 }, "moonshotai/kimi-k3": { - "input": 0.192, - "output": 15.0 + "input": 3.0, + "output": 15.0, + "basis": "Moonshot AI (the lab's own price)", + "discount_removed": 0.0 }, "nvidia/nemotron-3-nano-30b-a3b": { "input": 0.06, - "output": 0.24 + "output": 0.24, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "openai/gpt-4.1": { "input": 2.0, - "output": 8.0 + "output": 8.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-4.1-mini": { "input": 0.4, - "output": 1.6 + "output": 1.6, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-4.1-nano": { - "input": 0.1, - "output": 0.4 + "input": 0.105, + "output": 0.42, + "basis": "median of 2 host(s)", + "discount_removed": 0.0 }, "openai/gpt-4o": { "input": 2.5, - "output": 10.0 + "output": 10.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-4o-mini": { "input": 0.15, - "output": 0.6 + "output": 0.6, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5": { "input": 1.25, - "output": 10.0 + "output": 10.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5-mini": { "input": 0.25, - "output": 2.0 + "output": 2.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5-nano": { "input": 0.05, - "output": 0.4 + "output": 0.4, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.1": { "input": 1.25, - "output": 10.0 + "output": 10.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.2": { "input": 1.75, - "output": 14.0 + "output": 14.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.2-chat": { "input": 1.75, - "output": 14.0 + "output": 14.0, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "openai/gpt-5.4": { "input": 2.5, - "output": 15.0 + "output": 15.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.4-mini": { "input": 0.75, - "output": 4.5 + "output": 4.5, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.4-nano": { "input": 0.2, - "output": 1.25 + "output": 1.25, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.5": { "input": 5.0, - "output": 30.0 + "output": 30.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.6-luna": { "input": 0.2, - "output": 1.2 + "output": 1.2, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.6-sol": { "input": 4.0, - "output": 20.0 + "output": 20.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-5.6-terra": { "input": 2.0, - "output": 12.0 + "output": 12.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-6-luna": { "input": 0.1, - "output": 0.5 + "output": 0.5, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-6-sol": { "input": 2.0, - "output": 10.0 + "output": 10.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-6.1-sol": { "input": 2.0, - "output": 10.0 + "output": 10.0, + "basis": "OpenAI (the lab's own price)", + "discount_removed": 0.0 }, "openai/gpt-oss-120b": { "input": 0.15, - "output": 0.6 + "output": 0.6, + "basis": "median of 16 host(s)", + "discount_removed": 0.0 }, "openai/gpt-oss-20b": { - "input": 0.018, - "output": 0.09 + "input": 0.04, + "output": 0.15, + "basis": "median of 7 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-235b-a22b-2507": { - "input": 0.09, - "output": 0.55 + "input": 0.145, + "output": 0.775, + "basis": "median of 4 host(s)", + "discount_removed": 0.75 }, "qwen/qwen3-30b-a3b-instruct-2507": { - "input": 0.1, - "output": 0.3 + "input": 0.09, + "output": 0.3, + "basis": "median of 3 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-coder": { - "input": 0.3, - "output": 1.0 + "input": 0.35, + "output": 1.5, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-coder-30b-a3b-instruct": { - "input": 0.07, - "output": 0.28 + "input": 0.11, + "output": 0.44, + "basis": "median of 2 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-coder-next": { "input": 0.12, - "output": 0.8 + "output": 0.8, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-next-80b-a3b-instruct": { - "input": 0.09, - "output": 1.1 + "input": 0.095, + "output": 1.1, + "basis": "median of 2 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3-vl-235b-a22b-instruct": { "input": 0.21, - "output": 1.9 + "output": 1.9, + "basis": "median of 3 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3.5-9b": { - "input": 0.17, - "output": 0.25 + "input": 0.1, + "output": 0.15, + "basis": "median of 5 host(s)", + "discount_removed": 0.0 }, "qwen/qwen3.5-flash-02-23": { "input": 0.065, - "output": 0.26 + "output": 0.26, + "basis": "Alibaba (the lab's own price)", + "discount_removed": 0.0 }, "upstage/solar-pro4": { - "input": 0.15, - "output": 0.6 + "input": 0.3, + "output": 1.2, + "basis": "Upstage (the lab's own price)", + "discount_removed": 0.5 }, "x-ai/grok-4.3": { "input": 1.25, - "output": 2.5 + "output": 2.5, + "basis": "xAI (the lab's own price)", + "discount_removed": 0.0 }, "xiaomi/mimo-v2.5": { "input": 0.4, - "output": 2.0 + "output": 2.0, + "basis": "median of 1 host(s)", + "discount_removed": 0.0 }, "z-ai/glm-4.5-air": { - "input": 0.13, - "output": 0.85 + "input": 0.2, + "output": 1.1, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 }, "z-ai/glm-4.6": { - "input": 0.5, - "output": 2.0 + "input": 0.6, + "output": 2.2, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 }, "z-ai/glm-4.7": { "input": 0.6, - "output": 2.2 + "output": 2.2, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 }, "z-ai/glm-5": { "input": 1.0, - "output": 3.2 + "output": 3.2, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 }, "z-ai/glm-5.3": { - "input": 0.0699, - "output": 1.4 + "input": 1.4, + "output": 4.4, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 }, "z-ai/glm-5.3-flash": { - "input": 0.0149, - "output": 0.1543 + "input": 0.15, + "output": 0.5, + "basis": "Z.AI (the lab's own price)", + "discount_removed": 0.0 } } } diff --git a/scripts/pricing/sync_openrouter.py b/scripts/pricing/sync_openrouter.py index 34abfb98..44f70b8e 100644 --- a/scripts/pricing/sync_openrouter.py +++ b/scripts/pricing/sync_openrouter.py @@ -1,28 +1,41 @@ # SPDX-FileCopyrightText: Copyright contributors to the RouterArena project # SPDX-License-Identifier: Apache-2.0 -"""Sync model prices in ``model_cost/model_profiles.yaml`` with OpenRouter's list prices. - -For every profile that resolves to an OpenRouter model, the profile's price is set to -OpenRouter's current list price (prompt/completion, converted to USD per 1M tokens), and -``as_of`` and ``source`` are updated. ``model_cost.json`` is then regenerated. - -A profile resolves through its ``openrouter_id`` field, or else through an exact match of -its id or one of its aliases against OpenRouter's model ids. Set ``sync: false`` on a -profile to keep its price fixed (a vendor's standard price, a local model, ...). +"""Sync model prices in ``model_cost/model_profiles.yaml`` with each model's standard price on OpenRouter. + +A model is priced at its standard published price, not at a promotion and not at the +cheapest host of the day. For every profile that resolves to an OpenRouter model, the +price is read from OpenRouter's per-provider endpoints: + +* only standard-tier endpoints count (no Flex, Priority or batch tiers), and only active + ones when any are; +* a provider's promotional discount is undone (OpenRouter lists ``discount: 0.5`` next to + a halved price, so the standard price is ``price / (1 - discount)``); +* when the model's own lab sells it on OpenRouter (Google for Gemini, Z.AI for GLM, ...), + the lab's lowest standard price is used; +* otherwise the median over hosts is used, leaving out 4-bit quantizations when + higher-precision hosts exist. + +The profile's ``price``, ``as_of`` and ``source`` are updated and ``model_cost.json`` is +regenerated. A profile resolves through its ``openrouter_id`` field, or else through an +exact match of its id or one of its aliases against OpenRouter's model ids. Set +``sync: false`` on a profile to keep its price fixed (a retired model, a model OpenRouter +does not list, ...). A model whose endpoints cannot be read keeps its price and is listed +in the report. When a price changes, the run also writes ``model_cost/price_snapshots/.json`` with -the OpenRouter prices of every resolved model, so each change point is kept as history +the price and its basis for every resolved model, so each change point is kept as history (``--always-snapshot`` writes one even when nothing changed). Syncing changes the price table only. Leaderboard costs change when a maintainer re-prices the routers, as a separate step (scripts/pricing/reprice.py). Usage: - python scripts/pricing/sync_openrouter.py # fetch, update, write - python scripts/pricing/sync_openrouter.py --dry-run # report only - python scripts/pricing/sync_openrouter.py --models-json f # use a saved API response - python scripts/pricing/sync_openrouter.py --report out.md # also write a Markdown report + python scripts/pricing/sync_openrouter.py # fetch, update, write + python scripts/pricing/sync_openrouter.py --dry-run # report only + python scripts/pricing/sync_openrouter.py --models-json f # use a saved /models response + python scripts/pricing/sync_openrouter.py --endpoints-dir d # use saved endpoint responses + python scripts/pricing/sync_openrouter.py --report out.md # also write a Markdown report """ from __future__ import annotations @@ -31,18 +44,42 @@ import datetime as dt import json import re +import statistics import sys +import time import urllib.request from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple sys.path.insert(0, str(Path(__file__).resolve().parent)) import build_model_cost # noqa: E402 OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models" +OPENROUTER_ENDPOINTS_URL = "https://openrouter.ai/api/v1/models/{}/endpoints" SNAPSHOT_DIR = build_model_cost.REPO_ROOT / "model_cost" / "price_snapshots" +# The lab's own provider name(s) on OpenRouter, by the model id's vendor prefix. +FIRST_PARTY = { + "anthropic": {"Anthropic"}, + "deepseek": {"DeepSeek"}, + "google": {"Google", "Google AI Studio"}, + "inclusionai": {"inclusionAI"}, + "minimax": {"Minimax", "MiniMax"}, + "mistralai": {"Mistral"}, + "moonshotai": {"Moonshot AI"}, + "nvidia": {"NVIDIA"}, + "openai": {"OpenAI"}, + "qwen": {"Alibaba"}, + "upstage": {"Upstage"}, + "x-ai": {"xAI"}, + "xiaomi": {"Xiaomi"}, + "z-ai": {"Z.AI"}, +} +# Quantizations below 8 bits: a cheaper, degraded variant, not the model's standard serving. +LOW_BIT = {"fp4", "mxfp4", "nvfp4", "int4"} + Price = Tuple[float, float] +Endpoints = Optional[List[Dict[str, Any]]] def per_million(per_token: Any) -> Optional[float]: @@ -57,17 +94,15 @@ def per_million(per_token: Any) -> Optional[float]: def load_openrouter_prices(source: Optional[str] = None) -> Dict[str, Price]: - """Map OpenRouter model id -> (input, output) USD per 1M tokens. + """Map OpenRouter model id -> (input, output) list price, USD per 1M tokens. - ``source`` is a saved API response (JSON file); by default the live API is fetched.""" + Used to resolve profiles to OpenRouter ids; the price itself comes from + ``standard_price``. ``source`` is a saved API response (JSON file); by default the + live API is fetched.""" if source: payload = json.loads(Path(source).read_text(encoding="utf-8")) else: - request = urllib.request.Request( - OPENROUTER_MODELS_URL, headers={"User-Agent": "routerarena-price-sync"} - ) - with urllib.request.urlopen(request, timeout=60) as response: - payload = json.load(response) + payload = _get_json(OPENROUTER_MODELS_URL) prices: Dict[str, Price] = {} for model in payload.get("data", []): pricing = model.get("pricing") or {} @@ -79,6 +114,89 @@ def load_openrouter_prices(source: Optional[str] = None) -> Dict[str, Price]: return prices +def _get_json(url: str) -> Any: + request = urllib.request.Request( + url, headers={"User-Agent": "routerarena-price-sync"} + ) + with urllib.request.urlopen(request, timeout=60) as response: + return json.load(response) + + +def endpoints_loader(endpoints_dir: Optional[str]) -> Callable[[str], Endpoints]: + """Return a function that gives an OpenRouter id's endpoint list, or None if it + can't be read. Reads ``/__.json`` when a directory is given, + else the live API (with retries).""" + + def from_dir(or_id: str) -> Endpoints: + assert endpoints_dir is not None + path = Path(endpoints_dir) / f"{or_id.replace('/', '__')}.json" + if not path.exists(): + return None + payload = json.loads(path.read_text(encoding="utf-8")) + return (payload.get("data") or {}).get("endpoints") + + def from_api(or_id: str) -> Endpoints: + for attempt in range(3): + try: + payload = _get_json(OPENROUTER_ENDPOINTS_URL.format(or_id)) + return (payload.get("data") or {}).get("endpoints") + except Exception: + time.sleep(2 * (attempt + 1)) + return None + + return from_dir if endpoints_dir else from_api + + +def standard_price(or_id: str, endpoints: Endpoints) -> Optional[Dict[str, Any]]: + """The model's standard price from its OpenRouter endpoints (see the module + docstring), as {"price": (input, output), "basis": str, "discount_removed": float}, + or None if no standard-tier endpoint has a price.""" + standard = [ + e + for e in endpoints or [] + if e.get("service_tier") in (None, "standard") + and per_million((e.get("pricing") or {}).get("prompt")) is not None + and per_million((e.get("pricing") or {}).get("completion")) is not None + ] + active = [e for e in standard if (e.get("status") or 0) >= 0] + candidates = active or standard + if not candidates: + return None + + def undiscounted(endpoint: Dict[str, Any]) -> Tuple[float, float, float]: + pricing = endpoint["pricing"] + discount = float(pricing.get("discount") or 0) + factor = 1 / (1 - discount) if 0 < discount < 1 else 1.0 + return ( + round(float(pricing["prompt"]) * 1e6 * factor, 6), + round(float(pricing["completion"]) * 1e6 * factor, 6), + discount, + ) + + lab = FIRST_PARTY.get(or_id.split("/")[0], set()) + own = [e for e in candidates if e.get("provider_name") in lab] + if own: + best = min(own, key=lambda e: sum(undiscounted(e)[:2])) + i, o, d = undiscounted(best) + return { + "price": (i, o), + "basis": f"{best['provider_name']} (the lab's own price)", + "discount_removed": d, + } + hosts = [ + e for e in candidates if str(e.get("quantization")) not in LOW_BIT + ] or candidates + prices = [undiscounted(e) for e in hosts] + return { + "price": ( + round(statistics.median(p[0] for p in prices), 6), + round(statistics.median(p[1] for p in prices), 6), + ), + "basis": f"median of {len(prices)} host(s)", + "discount_removed": max(p[2] for p in prices), + } + + def resolve_openrouter_id( model_id: str, profile: Dict[str, Any], openrouter: Dict[str, Price] ) -> Optional[str]: @@ -93,16 +211,21 @@ def resolve_openrouter_id( def plan_updates( - profiles: Dict[str, Dict[str, Any]], openrouter: Dict[str, Price] -) -> Tuple[List[Dict[str, Any]], Dict[str, str], List[str]]: - """Return (changes, resolved, unresolved). - - ``changes`` lists profiles whose price differs from OpenRouter's; ``resolved`` maps - profile id -> OpenRouter id for every synced profile; ``unresolved`` lists profiles - that sync but match no OpenRouter model (or whose ``openrouter_id`` is unknown).""" + profiles: Dict[str, Dict[str, Any]], + openrouter: Dict[str, Price], + get_endpoints: Callable[[str], Endpoints], +) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]], List[str], List[str]]: + """Return (changes, resolved, unresolved, no_data). + + ``changes`` lists profiles whose price differs from the standard price; + ``resolved`` maps profile id -> {"openrouter_id", "price", "basis", + "discount_removed"} for every synced profile; ``unresolved`` lists profiles that sync + but match no OpenRouter model; ``no_data`` lists profiles whose endpoints could not + be read (they keep their price).""" changes: List[Dict[str, Any]] = [] - resolved: Dict[str, str] = {} + resolved: Dict[str, Dict[str, Any]] = {} unresolved: List[str] = [] + no_data: List[str] = [] for model_id, profile in profiles.items(): if profile.get("sync") is False: continue @@ -110,14 +233,25 @@ def plan_updates( if or_id is None: unresolved.append(model_id) continue - resolved[model_id] = or_id - new = openrouter[or_id] + found = standard_price(or_id, get_endpoints(or_id)) + if found is None: + no_data.append(model_id) + continue + resolved[model_id] = {"openrouter_id": or_id, **found} + new = found["price"] old = (float(profile["price"]["input"]), float(profile["price"]["output"])) if abs(old[0] - new[0]) > 1e-9 or abs(old[1] - new[1]) > 1e-9: changes.append( - {"model": model_id, "openrouter_id": or_id, "old": old, "new": new} + { + "model": model_id, + "openrouter_id": or_id, + "old": old, + "new": new, + "basis": found["basis"], + "discount_removed": found["discount_removed"], + } ) - return changes, resolved, unresolved + return changes, resolved, unresolved, no_data def _fmt(value: float) -> str: @@ -159,37 +293,64 @@ def apply_updates(text: str, changes: List[Dict[str, Any]], today: str) -> str: return "\n".join(lines) -def write_snapshot( - resolved: Dict[str, str], openrouter: Dict[str, Price], today: str -) -> Path: +def write_snapshot(resolved: Dict[str, Dict[str, Any]], today: str) -> Path: SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True) path = SNAPSHOT_DIR / f"{today}.json" - prices = { - or_id: {"input": openrouter[or_id][0], "output": openrouter[or_id][1]} - for or_id in sorted(set(resolved.values())) + prices = {} + for entry in sorted(resolved.values(), key=lambda e: e["openrouter_id"]): + prices[entry["openrouter_id"]] = { + "input": entry["price"][0], + "output": entry["price"][1], + "basis": entry["basis"], + "discount_removed": entry["discount_removed"], + } + payload = { + "date": today, + "source": OPENROUTER_ENDPOINTS_URL.format(""), + "rule": "standard price: the lab's own standard-tier price, else the median " + "over hosts (no 4-bit); promotional discounts undone", + "prices": prices, } - payload = {"date": today, "source": OPENROUTER_MODELS_URL, "prices": prices} path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") return path -def report(changes: List[Dict[str, Any]], unresolved: List[str], today: str) -> str: +def report( + changes: List[Dict[str, Any]], + unresolved: List[str], + no_data: List[str], + today: str, +) -> str: out = [f"## OpenRouter price sync, {today}", ""] if changes: out += [ f"{len(changes)} price(s) changed (USD per 1M tokens, input / output):", "", - "| Model | OpenRouter id | Old | New |", - "| --- | --- | --- | --- |", + "| Model | OpenRouter id | Old | New | Basis |", + "| --- | --- | --- | --- | --- |", ] for c in changes: + basis = c["basis"] + if c["discount_removed"]: + basis += f", {round(100 * c['discount_removed'])}% promotion undone" + ratio = max( + (c["new"][k] / c["old"][k]) if c["old"][k] else 0 for k in (0, 1) + ) + flag = " ⚠️" if ratio >= 2 or ratio <= 0.5 else "" out.append( - f"| `{c['model']}` | `{c['openrouter_id']}` | " + f"| `{c['model']}`{flag} | `{c['openrouter_id']}` | " f"{_fmt(c['old'][0])} / {_fmt(c['old'][1])} | " - f"{_fmt(c['new'][0])} / {_fmt(c['new'][1])} |" + f"{_fmt(c['new'][0])} / {_fmt(c['new'][1])} | {basis} |" ) + out += ["", "⚠️ marks a price that at least doubled or halved."] else: out.append("No price changed.") + if no_data: + out += [ + "", + f"{len(no_data)} profile(s) kept their price because OpenRouter's endpoints " + "could not be read: " + ", ".join(f"`{m}`" for m in no_data), + ] if unresolved: out += [ "", @@ -203,6 +364,10 @@ def report(changes: List[Dict[str, Any]], unresolved: List[str], today: str) -> def main(argv: Optional[List[str]] = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--models-json", help="saved OpenRouter /models response") + parser.add_argument( + "--endpoints-dir", + help="saved /models//endpoints responses, named __.json", + ) parser.add_argument("--date", default=dt.date.today().isoformat()) parser.add_argument( "--dry-run", action="store_true", help="report only; write nothing" @@ -217,8 +382,10 @@ def main(argv: Optional[List[str]] = None) -> int: profiles = build_model_cost.load_profiles(build_model_cost.PROFILES_PATH) openrouter = load_openrouter_prices(args.models_json) - changes, resolved, unresolved = plan_updates(profiles, openrouter) - text = report(changes, unresolved, args.date) + changes, resolved, unresolved, no_data = plan_updates( + profiles, openrouter, endpoints_loader(args.endpoints_dir) + ) + text = report(changes, unresolved, no_data, args.date) print(text) if args.report: Path(args.report).write_text(text, encoding="utf-8") @@ -226,7 +393,7 @@ def main(argv: Optional[List[str]] = None) -> int: return 0 if changes or args.always_snapshot: - write_snapshot(resolved, openrouter, args.date) + write_snapshot(resolved, args.date) if changes: path = build_model_cost.PROFILES_PATH path.write_text( diff --git a/tests/test_price_sync.py b/tests/test_price_sync.py index b1a986e5..ba9e815a 100644 --- a/tests/test_price_sync.py +++ b/tests/test_price_sync.py @@ -55,6 +55,42 @@ } +def endpoint( + provider, prompt, completion, discount=0, tier=None, quant="unknown", status=0 +): + return { + "provider_name": provider, + "service_tier": tier, + "quantization": quant, + "status": status, + "pricing": { + "prompt": str(prompt / 1e6), + "completion": str(completion / 1e6), + "discount": discount, + }, + } + + +# google/model-b is sold by its lab at a 50% introductory discount (standard 1.5 / 7.5); +# the model-level list price above is one regional endpoint's discounted price. +ENDPOINTS = { + "vendor/model-a": [endpoint("SomeHost", 1.0, 2.0)], + "google/model-b": [ + endpoint("Google AI Studio", 0.375, 1.875, 0.5, tier="flex"), + endpoint("Google AI Studio", 0.75, 3.75, 0.5), + endpoint("Google", 0.825, 4.125, 0.5), + endpoint("Google", 1.35, 6.75, 0.5, tier="priority"), + ], +} + + +def write_endpoints(directory, endpoints): + directory.mkdir(exist_ok=True) + for or_id, eps in endpoints.items(): + path = directory / f"{or_id.replace('/', '__')}.json" + path.write_text(json.dumps({"data": {"endpoints": eps}}), encoding="utf-8") + + @pytest.fixture def setup(tmp_path, monkeypatch): profiles = tmp_path / "model_profiles.yaml" @@ -64,9 +100,17 @@ def setup(tmp_path, monkeypatch): monkeypatch.setattr(build_model_cost, "PROFILES_PATH", profiles) monkeypatch.setattr(build_model_cost, "COST_PATH", tmp_path / "model_cost.json") monkeypatch.setattr(sync_openrouter, "SNAPSHOT_DIR", tmp_path / "snapshots") + write_endpoints(tmp_path / "endpoints", ENDPOINTS) return tmp_path, profiles, models +def run(tmp_path, models, *extra): + return sync_openrouter.main( + ["--models-json", str(models), "--endpoints-dir", str(tmp_path / "endpoints")] + + list(extra) + ) + + def test_prices_convert_to_per_million_and_skip_variable_prices(setup): _, _, models = setup prices = sync_openrouter.load_openrouter_prices(str(models)) @@ -75,28 +119,72 @@ def test_prices_convert_to_per_million_and_skip_variable_prices(setup): def test_plan_resolves_by_explicit_id_and_respects_sync_false(setup): - _, profiles, models = setup + tmp_path, profiles, models = setup loaded = build_model_cost.load_profiles(profiles) - changes, resolved, unresolved = sync_openrouter.plan_updates( - loaded, sync_openrouter.load_openrouter_prices(str(models)) + changes, resolved, unresolved, no_data = sync_openrouter.plan_updates( + loaded, + sync_openrouter.load_openrouter_prices(str(models)), + sync_openrouter.endpoints_loader(str(tmp_path / "endpoints")), ) - assert resolved == {"vendor/model-a": "vendor/model-a", "model-b": "google/model-b"} + assert {k: v["openrouter_id"] for k, v in resolved.items()} == { + "vendor/model-a": "vendor/model-a", + "model-b": "google/model-b", + } assert [c["model"] for c in changes] == ["model-b"] # model-a is unchanged assert unresolved == ["local-only"] # "pinned" is skipped, not unresolved + assert no_data == [] + + +def test_standard_price_is_the_labs_own_without_the_promotion(): + found = sync_openrouter.standard_price( + "google/model-b", ENDPOINTS["google/model-b"] + ) + assert found["price"] == (1.5, 7.5) + assert found["discount_removed"] == 0.5 + assert "Google AI Studio" in found["basis"] + + +def test_standard_price_without_the_lab_is_the_median_of_full_precision_hosts(): + eps = [ + endpoint("HostA", 0.14, 0.28, quant="fp8"), + endpoint("HostB", 0.0399, 0.0798, 0.715, quant="fp8"), # promo of 0.14 / 0.28 + endpoint("HostC", 0.2, 0.5), + endpoint("Cheap4bit", 0.01, 0.02, quant="fp4"), + endpoint("Down", 0.01, 0.01, status=-5), + ] + found = sync_openrouter.standard_price("deepseek/model-x", eps) + assert found["price"] == (0.14, 0.28) + assert found["basis"] == "median of 3 host(s)" + + +def test_only_low_bit_hosts_still_give_a_price(): + eps = [endpoint("A", 0.1, 0.2, quant="fp4"), endpoint("B", 0.3, 0.4, quant="int4")] + assert sync_openrouter.standard_price("x/y", eps)["price"] == (0.2, 0.3) + + +def test_no_standard_tier_endpoint_means_no_price(): + eps = [endpoint("OpenAI", 2, 10, tier="flex")] + assert sync_openrouter.standard_price("openai/m", eps) is None + assert sync_openrouter.standard_price("openai/m", None) is None + + +def test_unreadable_endpoints_keep_the_price(setup): + tmp_path, profiles, models = setup + (tmp_path / "endpoints" / "google__model-b.json").unlink() + run(tmp_path, models, "--date", "2026-10-30") + data = yaml.safe_load(profiles.read_text(encoding="utf-8"))["models"] + assert data["model-b"]["price"] == {"input": 0.5, "output": 3} def test_run_updates_only_changed_lines_and_writes_snapshot(setup): tmp_path, profiles, models = setup - assert ( - sync_openrouter.main(["--models-json", str(models), "--date", "2026-10-30"]) - == 0 - ) + assert run(tmp_path, models, "--date", "2026-10-30") == 0 text = profiles.read_text(encoding="utf-8") assert text.startswith("# A comment that must survive.") assert 'note: "keep me"' in text data = yaml.safe_load(text)["models"] - assert data["model-b"]["price"] == {"input": 0.825, "output": 4.125} + assert data["model-b"]["price"] == {"input": 1.5, "output": 7.5} assert data["model-b"]["as_of"] == "2026-10-30" assert data["model-b"]["source"] == "https://openrouter.ai/google/model-b" assert ( @@ -106,15 +194,16 @@ def test_run_updates_only_changed_lines_and_writes_snapshot(setup): snapshot = json.loads((tmp_path / "snapshots" / "2026-10-30.json").read_text()) assert set(snapshot["prices"]) == {"vendor/model-a", "google/model-b"} + assert snapshot["prices"]["google/model-b"]["discount_removed"] == 0.5 table = json.loads((tmp_path / "model_cost.json").read_text()) - assert table["model-b"]["output_token_price_per_million"] == 4.125 + assert table["model-b"]["output_token_price_per_million"] == 7.5 def test_dry_run_writes_nothing(setup): tmp_path, profiles, models = setup before = profiles.read_text(encoding="utf-8") - sync_openrouter.main(["--models-json", str(models), "--dry-run"]) + run(tmp_path, models, "--dry-run") assert profiles.read_text(encoding="utf-8") == before assert not (tmp_path / "snapshots").exists() @@ -137,12 +226,10 @@ def test_missing_source_and_as_of_lines_are_added(setup): def test_no_change_writes_no_snapshot_unless_asked(setup): tmp_path, profiles, models = setup - data = json.loads(models.read_text()) - data["data"][1]["pricing"] = {"prompt": "0.0000005", "completion": "0.000003"} - models.write_text(json.dumps(data)) - sync_openrouter.main(["--models-json", str(models), "--date", "2026-10-30"]) - assert not (tmp_path / "snapshots").exists() - sync_openrouter.main( - ["--models-json", str(models), "--date", "2026-10-30", "--always-snapshot"] + write_endpoints( + tmp_path / "endpoints", {"google/model-b": [endpoint("Google", 0.5, 3)]} ) + run(tmp_path, models, "--date", "2026-10-30") + assert not (tmp_path / "snapshots").exists() + run(tmp_path, models, "--date", "2026-10-30", "--always-snapshot") assert (tmp_path / "snapshots" / "2026-10-30.json").exists()