Setting the file. One moment.
Bedrock Pricing · LLM To Bedrock · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 81
Creating Amazon Aurora Db Cluster With Instances
104
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
Reference Run Source Model Baseline
scripts/ bedrock_pricing.py
Python · 210 lines · 12 KB
15
16
17 def parse_price_dimensions (price_item: dict ) -> dict :
18 """Pure: pull input/output per-1K-token USD rates from one PriceList item.
19 Only matches base input/output token dimensions — excludes cache read/write
20 and other extended dimensions that share the 'input'/'output' substring."""
21 inp = out = None
22 terms = price_item.get( "terms" , {}).get( "OnDemand" , {})
23 for term in terms.values():
24 for dim in term.get( "priceDimensions" , {}).values():
25 usd = float (dim.get( "pricePerUnit" , {}).get( "USD" , "0" ) or 0 )
26 desc = dim.get( "description" , "" ).lower()
27 if any (skip in desc for skip in ( "cache" , "read" , "write" , "batch" )):
28 continue
29 if "input" in desc and "token" in desc:
30 inp = usd
31 elif "output" in desc and "token" in desc:
32 out = usd
33 return { "input_per_1k_usd" : inp, "output_per_1k_usd" : out}
34
35
36 # Static fallback table: per-1K-token USD rates from public pricing pages.
37 # Used when the PriceList API doesn't return data (e.g. new cross-region inference profile IDs).
38 # Source: https://aws.amazon.com/bedrock/pricing/, cross-checked row-by-row against
39 # skills/gcp-to-aws/references/shared/pricing-cache.md (its per-1M rates / 1000).
40 # Every row below was re-verified against that cache on 2026-08-04; the Opus 4.8 row
41 # had been copied from Opus 4.1's legacy $15/$75 and was corrected to $5/$25.
42 # Re-check this table against that cache (and the public pricing page) whenever either moves.
43 STATIC_FALLBACK = {
44 "anthropic.claude-haiku-4-5-20251001-v1:0" : { "input_per_1k_usd" : 0.001 , "output_per_1k_usd" : 0.005 },
45 "us.anthropic.claude-haiku-4-5-20251001-v1:0" : { "input_per_1k_usd" : 0.001 , "output_per_1k_usd" : 0.005 },
46 # Recommend default
47 "anthropic.claude-sonnet-5" : { "input_per_1k_usd" : 0.002 , "output_per_1k_usd" : 0.010 },
48 "us.anthropic.claude-sonnet-5" : { "input_per_1k_usd" : 0.002 , "output_per_1k_usd" : 0.010 },
49 # Still Active — existing workloads / fallbacks
50 "anthropic.claude-sonnet-4-6" : { "input_per_1k_usd" : 0.003 , "output_per_1k_usd" : 0.015 },
51 "us.anthropic.claude-sonnet-4-6" : { "input_per_1k_usd" : 0.003 , "output_per_1k_usd" : 0.015 },
52
53 # Opus 4.8 has no dated foundation-model ID on the model card — suffix-less only.
54 "anthropic.claude-opus-4-8" : { "input_per_1k_usd" : 0.005 , "output_per_1k_usd" : 0.025 },
55 "us.anthropic.claude-opus-4-8" : { "input_per_1k_usd" : 0.005 , "output_per_1k_usd" : 0.025 },
56 "amazon.nova-micro-v1:0" : { "input_per_1k_usd" : 0.000035 , "output_per_1k_usd" : 0.00014 },
57 "amazon.nova-lite-v1:0" : { "input_per_1k_usd" : 0.00006 , "output_per_1k_usd" : 0.00024 },
58 "amazon.nova-pro-v1:0" : { "input_per_1k_usd" : 0.0008 , "output_per_1k_usd" : 0.0032 },
59 # OpenAI proprietary GPT models, SHORT-CONTEXT (272K) tier. Read off the model
60 # cards 2026-08-21. The PriceList API carries no GPT-5.x rows, so this table is
61 # the ONLY source. Pricing has an inference-option dimension:
62 # - bare mantle ids and Geo CRIS (us./in. prefixed): 1.10x OpenAI's standard
63 # list price (parity with OpenAI's *data residency* tier)
64 # - Global CRIS (global. prefixed, GPT-5.6 only): OpenAI's standard list
65 # price — cost PARITY, for workloads with no residency constraint
66 # The GPT-5.6 family also has a LONG-CONTEXT (1M) tier at 2.0x input / 1.5x
67 # output per option, NOT represented here — a >272K workload priced from this
68 # table is understated. GPT-5.5 and GPT-5.4: mantle-only, no CRIS, no 1M tier.
69 "openai.gpt-5.6-sol" : { "input_per_1k_usd" : 0.0044 , "output_per_1k_usd" : 0.022 },
70 "openai.gpt-5.6-terra" : { "input_per_1k_usd" : 0.0022 , "output_per_1k_usd" : 0.0132 },
71 "openai.gpt-5.6-luna" : { "input_per_1k_usd" : 0.00022 , "output_per_1k_usd" : 0.00132 },
72 "openai.gpt-5.5" : { "input_per_1k_usd" : 0.0055 , "output_per_1k_usd" : 0.033 },
73 "openai.gpt-5.4" : { "input_per_1k_usd" : 0.00275 , "output_per_1k_usd" : 0.0165 },
74 # GPT-5.6 CRIS profile ids (bedrock-runtime). Geo = data-residency tier
75 # (same as in-region); Global = standard-price parity.
76 "us.openai.gpt-5.6-sol" : { "input_per_1k_usd" : 0.0044 , "output_per_1k_usd" : 0.022 },
77 "us.openai.gpt-5.6-terra" : { "input_per_1k_usd" : 0.0022 , "output_per_1k_usd" : 0.0132 },
78 "us.openai.gpt-5.6-luna" : { "input_per_1k_usd" : 0.00022 , "output_per_1k_usd" : 0.00132 },
79 "in.openai.gpt-5.6-terra" : { "input_per_1k_usd" : 0.0022 , "output_per_1k_usd" : 0.0132 },
80 "in.openai.gpt-5.6-luna" : { "input_per_1k_usd" : 0.00022 , "output_per_1k_usd" : 0.00132 },
81 "global.openai.gpt-5.6-sol" : { "input_per_1k_usd" : 0.004 , "output_per_1k_usd" : 0.020 },
82 "global.openai.gpt-5.6-terra" : { "input_per_1k_usd" : 0.002 , "output_per_1k_usd" : 0.012 },
83 "global.openai.gpt-5.6-luna" : { "input_per_1k_usd" : 0.0002 , "output_per_1k_usd" : 0.0012 },
84 }
85
86
87 def is_mantle_gpt (model_id: str ) -> bool :
88 """Pure: OpenAI's proprietary GPT models, which the AWS PriceList API does not
89 carry. The open-weight gpt-oss models ARE in the PriceList API and must not match."""
90 mid = model_id.lower()
91 return mid.startswith( "openai.gpt-5" ) and "oss" not in mid
92
93
94 def unavailable (note: str ) -> dict :
95 return { "available" : False , "input_per_1k_usd" : None ,
96 "output_per_1k_usd" : None , "note" : f "Pricing unavailable: { note } " }
97
98
99 def _static_fallback (model_id: str ) -> dict | None :
100 """Try the static fallback table. Returns a result dict or None."""
101 entry = STATIC_FALLBACK .get(model_id)
102 if entry:
103 return { ** entry, "available" : True , "note" : "static fallback (PriceList API had no entry)" }
104 # Proprietary GPT ids require an EXACT match, in every form (bare mantle id or
105 # us./in./global. CRIS profile). Tier names differ only by suffix at very
106 # different price points, and the inference options differ by prefix at a 10%
107 # spread — a partial match on e.g. `openai.gpt-5.6` or `us.openai.gpt-5.6`
108 # would silently bill one tier or option at another's rate.
109 if is_mantle_gpt(model_id) or re.match( r " ^( us | in | global ) \. openai \. gpt-5" , model_id):
110 return None
111 # Try stripping the version suffix for a partial match (e.g. us.anthropic.claude-sonnet-5)
112 base = model_id.rsplit( "-v" , 1 )[ 0 ] if "-v" in model_id else model_id
113 for key, val in STATIC_FALLBACK .items():
114 # Bidirectional: a dateless query must match a dated table key
115 # (key startswith base) AND a date-pinned query must match a dateless
116 # family key (base startswith key + "-"; the separator guard keeps
117 # ...opus-4-85 from matching the ...opus-4-8 family).
118 if key.startswith(base) or base == key or base.startswith(key + "-" ):
119 return { ** val, "available" : True , "note" : f "static fallback (matched { key } )" }
120 # Also strip a trailing date stamp (e.g. ...-sonnet-4-6-20250514 -> ...-sonnet-4-6) so a
121 # dated ID form still matches the undated table keys.
122 dateless = re.sub( r "- \d {8} $ " , "" , base)
123 if dateless != base:
124 for key, val in STATIC_FALLBACK .items():
125 if key.startswith(dateless):
126 return { ** val, "available" : True , "note" : f "static fallback (matched { key } )" }
127 return None
128
129
130 def display_name_guess (model_id: str ) -> str :
131 """Pure: derive a Pricing-API display-name guess from a Bedrock model id.
132 'us.anthropic.claude-haiku-4-5-20251001-v1:0' -> 'Claude Haiku 4.5'.
133 The Pricing API's 'model' attribute holds display names, not model ids."""
134 base = model_id.split( ":" , 1 )[ 0 ]
135 base = re.sub( r " ^( us | eu | apac | global ) \. " , "" , base)
136 base = base.split( "." , 1 )[ - 1 ] # drop vendor prefix
137 base = re.sub( r "-v \d + $ " , "" , base) # drop -v1
138 base = re.sub( r "- \d {8} $ " , "" , base) # drop date stamp
139 words = []
140 for tok in base.split( "-" ):
141 if tok.isdigit():
142 # version digits join with '.' (4-5 -> 4.5)
143 if words and re.match( r " ^\d[\d . ] * $ " , words[ - 1 ]):
144 words[ - 1 ] = f " { words[ - 1 ] } . { tok } "
145 else :
146 words.append(tok)
147 else :
148 words.append(tok.capitalize())
149 return " " .join(words)
150
151
152 def lookup (region: str , model_id: str ) -> dict :
153 # Curated static table is the primary source — the Pricing API keys models
154 # by display name and frequently lacks entries for new inference profiles.
155 fb = _static_fallback(model_id)
156 if fb:
157 fb[ "note" ] = ( "static pricing table (verified 2026-08-04 against "
158 "aws.amazon.com/bedrock/pricing and the vendored pricing cache)" )
159 return fb
160 if is_mantle_gpt(model_id):
161 # Short-circuit: the PriceList API carries no rows for the proprietary GPT
162 # models, so a live lookup would burn a round trip and still return nothing —
163 # and a generic "unavailable" would read as "this model doesn't exist".
164 return unavailable(
165 f "the AWS PriceList API does not carry OpenAI's proprietary GPT models, and "
166 f " { model_id } is not in the static table. This does NOT mean the model is "
167 f "unavailable. Read the rate from the OpenAI tab of "
168 f "aws.amazon.com/bedrock/pricing and add it to STATIC_FALLBACK. Do not derive "
169 f "it from a percentage change to an older rate." )
170 import boto3
171 from botocore.exceptions import BotoCoreError, ClientError
172 try :
173 # Best-effort live lookup for models not in the static table.
174 # Pricing API is only served from us-east-1 / ap-south-1.
175 client = boto3.client( "pricing" , region_name = "us-east-1" )
176 resp = client.get_products(
177 ServiceCode = "AmazonBedrock" ,
178 Filters = [
179 { "Type" : "TERM_MATCH" , "Field" : "model" , "Value" : display_name_guess(model_id)},
180 { "Type" : "TERM_MATCH" , "Field" : "regionCode" , "Value" : region},
181 ],
182 MaxResults = 1 ,
183 )
184 items = resp.get( "PriceList" , [])
185 if not items:
186 return unavailable(
187 f "not in static table and no PriceList entry for display name "
188 f "' { display_name_guess(model_id) } ' in { region } " )
189 parsed = parse_price_dimensions(json.loads(items[ 0 ]))
190 parsed[ "available" ] = parsed[ "input_per_1k_usd" ] is not None
191 parsed[ "note" ] = ( f "live Pricing API (matched display name ' { display_name_guess(model_id) } ')"
192 if parsed[ "available" ] else "rates not found in price item" )
193 return parsed
194 except (BotoCoreError, ClientError, ValueError , TypeError , AttributeError ) as e:
195 return unavailable( str (e))
196
197
198 def main (argv = None ) -> int :
199 ap = argparse.ArgumentParser()
200 ap.add_argument( "--region" , required = True )
201 ap.add_argument( "--models" , required = True )
202 args = ap.parse_args(argv)
203 out = {m.strip(): lookup(args.region, m.strip())
204 for m in args.models.split( "," ) if m.strip()}
205 print (json.dumps(out, indent = 2 ))
206 return 0
207
208
209 if __name__ == "__main__" :
210 sys.exit(main())