| 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) |