main
py 153 lines 6.16 KB
Raw
1 """GitHub Copilot model pricing helpers.
2
3 Source: https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing
4 Fetched: 2026-06-06. Prices are USD per 1M tokens and must be reviewed every two months.
5 """
6
7 from __future__ import annotations
8
9 from dataclasses import dataclass
10
11 PRICING_SOURCE_URL = (
12 "https://docs.github.com/en/copilot/reference/copilot-billing/models-and-pricing"
13 )
14 PRICING_FETCHED_DATE = "2026-06-06"
15 PRICING_REVIEW_INTERVAL_MONTHS = 2
16
17
18 @dataclass(frozen=True)
19 class ModelRate:
20 input: float
21 cached_input: float
22 output: float
23 cache_write: float | None = None
24
25 def as_dict(self) -> dict[str, float]:
26 values = {
27 "input": self.input,
28 "cached_input": self.cached_input,
29 "output": self.output,
30 }
31 if self.cache_write is not None:
32 values["cache_write"] = self.cache_write
33 return values
34
35
36 @dataclass(frozen=True)
37 class TieredModelRate:
38 default: ModelRate
39 long_context: ModelRate
40 long_context_threshold: int
41
42 def rate_for(self, input_tokens: int) -> ModelRate:
43 return self.long_context if input_tokens > self.long_context_threshold else self.default
44
45 def as_dict(self) -> dict[str, float | int]:
46 values: dict[str, float | int] = {
47 "input": self.default.input,
48 "cached_input": self.default.cached_input,
49 "output": self.default.output,
50 }
51 if self.default.cache_write is not None:
52 values["cache_write"] = self.default.cache_write
53 values["long_context_threshold"] = self.long_context_threshold
54 values["long_context_input"] = self.long_context.input
55 values["long_context_cached_input"] = self.long_context.cached_input
56 values["long_context_output"] = self.long_context.output
57 return values
58
59
60 SONNET_RATE = ModelRate(input=3.00, cached_input=0.30, cache_write=3.75, output=15.00)
61 OPUS_RATE = ModelRate(input=5.00, cached_input=0.50, cache_write=6.25, output=25.00)
62
63 MODEL_PRICING: dict[str, ModelRate | TieredModelRate] = {
64 "copilot-default": SONNET_RATE,
65 "gpt-5-mini": ModelRate(input=0.25, cached_input=0.025, output=2.00),
66 "openai/gpt-5-mini": ModelRate(input=0.25, cached_input=0.025, output=2.00),
67 "gpt-5.3-codex": ModelRate(input=1.75, cached_input=0.175, output=14.00),
68 "openai/gpt-5.3-codex": ModelRate(input=1.75, cached_input=0.175, output=14.00),
69 "gpt-5.4": TieredModelRate(
70 default=ModelRate(input=2.50, cached_input=0.25, output=15.00),
71 long_context=ModelRate(input=5.00, cached_input=0.50, output=22.50),
72 long_context_threshold=272_000,
73 ),
74 "openai/gpt-5.4": TieredModelRate(
75 default=ModelRate(input=2.50, cached_input=0.25, output=15.00),
76 long_context=ModelRate(input=5.00, cached_input=0.50, output=22.50),
77 long_context_threshold=272_000,
78 ),
79 "gpt-5.4-mini": ModelRate(input=0.75, cached_input=0.075, output=4.50),
80 "openai/gpt-5.4-mini": ModelRate(input=0.75, cached_input=0.075, output=4.50),
81 "gpt-5.4-nano": ModelRate(input=0.20, cached_input=0.02, output=1.25),
82 "openai/gpt-5.4-nano": ModelRate(input=0.20, cached_input=0.02, output=1.25),
83 "gpt-5.5": TieredModelRate(
84 default=ModelRate(input=5.00, cached_input=0.50, output=30.00),
85 long_context=ModelRate(input=10.00, cached_input=1.00, output=45.00),
86 long_context_threshold=272_000,
87 ),
88 "openai/gpt-5.5": TieredModelRate(
89 default=ModelRate(input=5.00, cached_input=0.50, output=30.00),
90 long_context=ModelRate(input=10.00, cached_input=1.00, output=45.00),
91 long_context_threshold=272_000,
92 ),
93 "claude-haiku-4.5": ModelRate(input=1.00, cached_input=0.10, cache_write=1.25, output=5.00),
94 "claude-sonnet-4": SONNET_RATE,
95 "claude-sonnet-4.5": SONNET_RATE,
96 "claude-sonnet-4.6": SONNET_RATE,
97 "claude-opus-4.5": OPUS_RATE,
98 "claude-opus-4.6": OPUS_RATE,
99 "claude-opus-4.7": OPUS_RATE,
100 "claude-opus-4.8": OPUS_RATE,
101 "gemini-2.5-pro": ModelRate(input=1.25, cached_input=0.125, output=10.00),
102 "google/gemini-2.5-pro": ModelRate(input=1.25, cached_input=0.125, output=10.00),
103 "gemini-3-flash": ModelRate(input=0.50, cached_input=0.05, output=3.00),
104 "google/gemini-3-flash": ModelRate(input=0.50, cached_input=0.05, output=3.00),
105 "gemini-3.1-pro": TieredModelRate(
106 default=ModelRate(input=2.00, cached_input=0.20, output=12.00),
107 long_context=ModelRate(input=4.00, cached_input=0.40, output=18.00),
108 long_context_threshold=200_000,
109 ),
110 "google/gemini-3.1-pro": TieredModelRate(
111 default=ModelRate(input=2.00, cached_input=0.20, output=12.00),
112 long_context=ModelRate(input=4.00, cached_input=0.40, output=18.00),
113 long_context_threshold=200_000,
114 ),
115 "gemini-3.5-flash": ModelRate(input=1.50, cached_input=0.15, output=9.00),
116 "google/gemini-3.5-flash": ModelRate(input=1.50, cached_input=0.15, output=9.00),
117 "raptor-mini": ModelRate(input=0.25, cached_input=0.025, output=2.00),
118 "github/raptor-mini": ModelRate(input=0.25, cached_input=0.025, output=2.00),
119 "mai-code-1-flash": ModelRate(input=0.75, cached_input=0.075, output=4.50),
120 "microsoft/mai-code-1-flash": ModelRate(input=0.75, cached_input=0.075, output=4.50),
121 }
122
123 MODEL_RATES = {model: pricing.as_dict() for model, pricing in MODEL_PRICING.items()}
124
125
126 def get_model_rate(model: str, input_tokens: int) -> dict[str, float] | None:
127 pricing = MODEL_PRICING.get(model)
128 if pricing is None:
129 return None
130 if isinstance(pricing, TieredModelRate):
131 return pricing.rate_for(input_tokens).as_dict()
132 return pricing.as_dict()
133
134
135 def estimate_cost_usd(
136 model: str,
137 input_tokens: int,
138 output_tokens: int,
139 cached_input_tokens: int = 0,
140 cache_write_tokens: int = 0,
141 ) -> float | None:
142 rates = get_model_rate(model, input_tokens)
143 if rates is None:
144 return None
145 if cache_write_tokens > 0 and "cache_write" not in rates:
146 return None
147 total = (
148 input_tokens * rates["input"]
149 + cached_input_tokens * rates["cached_input"]
150 + cache_write_tokens * rates.get("cache_write", 0)
151 + output_tokens * rates["output"]
152 ) / 1_000_000
153 return round(total, 6)