@pipeworx/mcp-medicare-pricing 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -6
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/fqhc-gaf.ts +332 -0
- package/src/index.ts +596 -6
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ What Medicare **pays** — physician fee schedule, lab rates, and per-day unit
|
|
|
4
4
|
limits. The companion to `medicare-coverage`, which answers whether something is
|
|
5
5
|
covered but not what it is worth.
|
|
6
6
|
|
|
7
|
-
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to
|
|
7
|
+
Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1882+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
@@ -14,6 +14,7 @@ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents
|
|
|
14
14
|
| `medicare_lab_rate` | "What does a comprehensive metabolic panel reimburse?" — national CLFS rate. |
|
|
15
15
|
| `medicare_lab_test_lookup` | "What does Medicare pay for Signatera, and which MolDX LCD applies?" — brand name → PLA code, current CLFS rate, and the MolDX LCDs/articles for the test's category. |
|
|
16
16
|
| `medicare_dmepos_rate` | "How much does Medicare pay for a CPAP machine (E0601) in Texas?" — state-priced, rural vs non-rural. |
|
|
17
|
+
| `medicare_opps_rate` | "What does Medicare pay a hospital outpatient department for CPT 33289 (CardioMEMS implant)?" — status indicator, APC, relative weight, national payment rate, copay, device/drug pass-through status. |
|
|
17
18
|
| `medicare_code_units_limit` | "How many units of this code will they pay for in one day?" — NCCI MUE. |
|
|
18
19
|
| `check_code_pair` | "Can I bill 00142 and 64474 together?" — NCCI PTP edit + modifier indicator for a code pair. |
|
|
19
20
|
| `explain_ncci_edit` | "What is bundled into (or bundles into) this code?" — every recorded PTP edit for one code. |
|
|
@@ -31,6 +32,7 @@ All from CMS, all downloadable files with no API:
|
|
|
31
32
|
- **Physician Fee Schedule relative value files** (RVUs + GPCIs) — annual
|
|
32
33
|
- **Clinical Laboratory Fee Schedule** — quarterly
|
|
33
34
|
- **DMEPOS fee schedule** (durable medical equipment, prosthetics, orthotics, supplies) — quarterly (as necessary): A=Jan, B=Apr, C=Jul, D=Oct
|
|
35
|
+
- **OPPS Addendum B** (Hospital Outpatient Prospective Payment System payment rates by HCPCS code) — quarterly: Q1=Jan, Q2=Apr, Q3=Jul, Q4=Oct
|
|
34
36
|
- **NCCI Medically Unlikely Edits** — quarterly
|
|
35
37
|
- **NCCI PTP quarterly additions/deletions/revisions files** — quarterly (see below — a narrower source than the other three)
|
|
36
38
|
- **FQHC Prospective Payment System base payment rate** — annual (see below — a hand-maintained table, not a downloaded file)
|
|
@@ -49,9 +51,20 @@ prior_year_rate_usd, transmittal, change_request, publication}` in
|
|
|
49
51
|
directly off its own primary CMS transmittal.
|
|
50
52
|
|
|
51
53
|
This is the national BASE rate only — before the per-FQHC Geographic
|
|
52
|
-
Adjustment Factor (GAF) CMS applies on top. The GAF
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
Adjustment Factor (GAF) CMS applies on top. The GAF-adjusted amount for a
|
|
55
|
+
locality is `medicare_fqhc_gaf` (below).
|
|
56
|
+
|
|
57
|
+
## Geographic adjustment: `medicare_gpci_lookup` and `medicare_fqhc_gaf` (fleet #3398)
|
|
58
|
+
|
|
59
|
+
`medicare_gpci_lookup` returns the work / practice-expense / malpractice GPCIs
|
|
60
|
+
for a locality or a whole state with no procedure code (years 2024-2026 loaded;
|
|
61
|
+
2024 and 2025 were backfilled with `scripts/ingest-medicare-pricing.mjs
|
|
62
|
+
--gpci-only`). `medicare_fqhc_gaf` returns CMS's own published FQHC GAF per
|
|
63
|
+
locality for CY2024-2026 and the GAF-adjusted per-visit rate. The GAF table is
|
|
64
|
+
CMS's published number, not derived here, and is a small generated source file
|
|
65
|
+
(`src/fqhc-gaf.ts`, 109 localities per year). **Refresh** each January: download
|
|
66
|
+
the new year's FQHC GAF file from CMS and run
|
|
67
|
+
`node scripts/build-fqhc-gaf-table.mjs 2024=<csv> 2025=<csv> 2026=<csv> 2027=<csv>`.
|
|
55
68
|
|
|
56
69
|
**Refresh**: CMS issues the following year's transmittal in Nov/Dec (the
|
|
57
70
|
CY2026 update was dated 2025-12-05 for a Jan 1, 2026 effective date). Add a
|
|
@@ -237,6 +250,48 @@ which is AMA copyright. The DMEPOS source file's own DMEREAD/DMEBACK
|
|
|
237
250
|
documentation carries no AMA/copyright notice anywhere (checked), so
|
|
238
251
|
`medicare_dmepos_rate` DOES return the code description.
|
|
239
252
|
|
|
253
|
+
## OPPS — what a hospital outpatient department is paid
|
|
254
|
+
|
|
255
|
+
`medicare_opps_rate` answers the same "what does Medicare pay" question for a
|
|
256
|
+
**hospital outpatient department** claim, as opposed to a physician office
|
|
257
|
+
(`medicare_physician_payment`) or a lab (`medicare_lab_rate`). Source: CMS,
|
|
258
|
+
OPPS Addendum B, the quarterly national payment-rate file by HCPCS code.
|
|
259
|
+
Measured size: 19,278 rows for the CY2026 October release — small, loaded
|
|
260
|
+
whole, same shape as `clfs_rates`/`dmepos_rates` already in this pack.
|
|
261
|
+
|
|
262
|
+
**Device and drug/biological pass-through is the status indicator, not a
|
|
263
|
+
separate column.** CMS's own "Drug and Device Pass-Through Expiration during
|
|
264
|
+
Calendar Year" column in Addendum B is, in practice, not a usable date field —
|
|
265
|
+
checked against the CY2026 October release, every non-blank value in that
|
|
266
|
+
column was a stray change-marker rather than a date. The reliable signal is
|
|
267
|
+
the status indicator itself: **SI='H'** is a pass-through device category
|
|
268
|
+
(paid separately, outside the standard APC amount, for as long as the device
|
|
269
|
+
holds that status — typically 2-3 years from market entry, after which CMS
|
|
270
|
+
reprices it, often to packaged SI='N'), and **SI='G'** is a pass-through
|
|
271
|
+
drug/biological. `device_pass_through`/`drug_or_biological_pass_through` on
|
|
272
|
+
every response are derived from SI, not from the expiration column.
|
|
273
|
+
|
|
274
|
+
A **packaged** code (SI='N', among several other status indicators) returns a
|
|
275
|
+
valid `status_indicator` and `status_indicator_meaning` but `apc: null` and no
|
|
276
|
+
payment rate — that is a correct, normal answer: OPPS pays for that code only
|
|
277
|
+
as part of whatever procedure it is billed alongside, never on its own.
|
|
278
|
+
CardioMEMS (C2624, the implantable pulmonary-artery pressure sensor) is a
|
|
279
|
+
worked example of exactly this: it has been on the market long enough that it
|
|
280
|
+
is no longer pass-through (SI='N', packaged into the implant procedure
|
|
281
|
+
33289's payment), which is the expected, current state — not a missing-data
|
|
282
|
+
gap.
|
|
283
|
+
|
|
284
|
+
## No OPPS short descriptors — a mixed-code-type file
|
|
285
|
+
|
|
286
|
+
Addendum B's own header states *"CPT codes and descriptions only are
|
|
287
|
+
copyright ... American Medical Association"*, and the file mixes AMA-
|
|
288
|
+
copyright CPT (HCPCS Level I, numeric codes like 33289) with CMS-owned HCPCS
|
|
289
|
+
Level II codes (like C2624) under **one shared** descriptor column, with no
|
|
290
|
+
per-row schema signal to tell which is which. Unlike `medicare_dmepos_rate`
|
|
291
|
+
(Level II only, so the description is safely CMS-owned), `medicare_opps_rate`
|
|
292
|
+
follows the stricter `pfs_rvu` convention instead: no descriptor is stored at
|
|
293
|
+
all, for any code in this table. A caller passes a code they already hold.
|
|
294
|
+
|
|
240
295
|
## DMEPOS pays by state, not locality
|
|
241
296
|
|
|
242
297
|
Medicare pays DMEPOS by **state** (not by the physician-fee-schedule
|
|
@@ -271,15 +326,30 @@ node scripts/ingest-ncci-ptp-baseline.mjs --dir /path/to/8-zips # full PTP base
|
|
|
271
326
|
|
|
272
327
|
node scripts/ingest-dmepos.mjs # newest DMEPOS release (default: 2026 release C)
|
|
273
328
|
node scripts/ingest-dmepos.mjs --year 2026 --release D # add a new release when CMS posts one
|
|
329
|
+
|
|
330
|
+
node scripts/ingest-opps.mjs # newest OPPS Addendum B release (default: 2026 October)
|
|
331
|
+
node scripts/ingest-opps.mjs --year 2026 --month january # add a new quarterly release when CMS posts one
|
|
274
332
|
```
|
|
275
333
|
|
|
276
334
|
Loaded as of writing: 19,226 RVU rows, 109 localities, 2,081 lab rates, 3,531
|
|
277
|
-
DMEPOS code/modifier rows (2026 release C, 53 states/territories each),
|
|
335
|
+
DMEPOS code/modifier rows (2026 release C, 53 states/territories each), 19,278
|
|
336
|
+
OPPS Addendum B rows (CY2026 October / Q4), 33,354
|
|
278
337
|
MUEs, **4,109,749 distinct NCCI PTP pairs** — the FULL CMS baseline (2026q3,
|
|
279
338
|
v322r0) with the 2025q1..2026q3 quarterly change files layered on top (fleet
|
|
280
339
|
#1293, 2026-09-07; superseded the prior 182,286-pair change-window-only
|
|
281
340
|
coverage). All loaders refuse rather than reporting an empty parse as success.
|
|
282
341
|
|
|
342
|
+
`ingest-opps.mjs` resolves the current download from the OPPS Addendum B
|
|
343
|
+
release landing page, same pattern as `ingest-dmepos.mjs`. The landing page's
|
|
344
|
+
own link routes through CMS's `/apps/ama/license.asp?file=...` AMA licence
|
|
345
|
+
click-through page — but, unlike the NCCI PTP baseline elsewhere in this pack,
|
|
346
|
+
the underlying file is NOT actually gated: fetching the plain
|
|
347
|
+
`https://www.cms.gov/files/zip/<name>.zip` URL directly (bypassing
|
|
348
|
+
license.asp entirely) returns the zip with no click-through, no cookie, and no
|
|
349
|
+
Referer needed — verified live, 2026-10-21. The script extracts that direct
|
|
350
|
+
URL from the landing page and fetches it, rather than going through
|
|
351
|
+
license.asp at all.
|
|
352
|
+
|
|
283
353
|
`ingest-dmepos.mjs` resolves the current download from the DMEPOS release
|
|
284
354
|
subpage rather than guessing the zip filename — CMS's DMEPOS naming is not
|
|
285
355
|
stable (a release page 404s before it is posted, and old release URLs have
|
|
@@ -337,7 +407,7 @@ directly, instead of just this one's:
|
|
|
337
407
|
}
|
|
338
408
|
```
|
|
339
409
|
|
|
340
|
-
Both URLs reach the same gateway and the same
|
|
410
|
+
Both URLs reach the same gateway and the same 1882+ data sources. The
|
|
341
411
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
342
412
|
reaches all of them from either one.
|
|
343
413
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-medicare-pricing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit limits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@cloudflare/workers-types": "^4.20260405.1"
|
|
27
27
|
},
|
|
28
28
|
"pipeworx": {
|
|
29
|
-
"sourceHash": "v1-
|
|
30
|
-
"sourceCommit": "
|
|
29
|
+
"sourceHash": "v1-510fd66ca4a1743183dd5075ecc73df84758f6eeef177a53b00aaae2036235fe",
|
|
30
|
+
"sourceCommit": "ba9dd6df5992aeb16ef2425ee86d1752c34ca636"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.pipeworx-io/medicare-pricing",
|
|
4
4
|
"title": "Medicare Pricing",
|
|
5
5
|
"description": "What does Medicare PAY — physician fee schedule, lab rates, and per-day unit limits.",
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.5",
|
|
7
7
|
"websiteUrl": "https://pipeworx.io/packs/medicare-pricing",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/pipeworx-io/mcp-medicare-pricing",
|
package/src/fqhc-gaf.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// GENERATED by scripts/build-fqhc-gaf-table.mjs -- do not hand-edit.
|
|
2
|
+
// Publisher: CMS, FQHC Prospective Payment System Geographic Adjustment Factors
|
|
3
|
+
// (one table per calendar year, "with 1.0 floor"). [year, state, locality_number, locality_name, gaf]
|
|
4
|
+
export const FQHC_GAF_ROWS: ReadonlyArray<readonly [number, string, string, string, number]> = [
|
|
5
|
+
[2024,"AL","00","ALABAMA",0.899],
|
|
6
|
+
[2024,"AK","01","ALASKA",1.246],
|
|
7
|
+
[2024,"AZ","00","ARIZONA",0.947],
|
|
8
|
+
[2024,"AR","13","ARKANSAS",0.894],
|
|
9
|
+
[2024,"CA","54","BAKERSFIELD",1.009],
|
|
10
|
+
[2024,"CA","55","CHICO",1.008],
|
|
11
|
+
[2024,"CA","71","EL CENTRO",1.008],
|
|
12
|
+
[2024,"CA","56","FRESNO",1.008],
|
|
13
|
+
[2024,"CA","57","HANFORD-CORCORAN",1.008],
|
|
14
|
+
[2024,"CA","18","LOS ANGELES-LONG BEACH-ANAHEIM (LOS ANGELES/ORANGE CNTY)",1.068],
|
|
15
|
+
[2024,"CA","58","MADERA",1.008],
|
|
16
|
+
[2024,"CA","59","MERCED",1.008],
|
|
17
|
+
[2024,"CA","60","MODESTO",1.008],
|
|
18
|
+
[2024,"CA","51","NAPA",1.129],
|
|
19
|
+
[2024,"CA","17","OXNARD-THOUSAND OAKS-VENTURA",1.055],
|
|
20
|
+
[2024,"CA","61","REDDING",1.008],
|
|
21
|
+
[2024,"CA","62","RIVERSIDE-SAN BERNARDINO-ONTARIO",1.008],
|
|
22
|
+
[2024,"CA","63","SACRAMENTO-ROSEVILLE-FOLSOM",1.047],
|
|
23
|
+
[2024,"CA","64","SALINAS",1.051],
|
|
24
|
+
[2024,"CA","72","SAN DIEGO-CHULA VISTA-CARLSBAD",1.059],
|
|
25
|
+
[2024,"CA","05","SAN FRANCISCO-OAKLAND-BERKELEY (SAN FRANCISCO/SAN MATEO/ALAMEDA/CONTRA COSTA CNTY)",1.194],
|
|
26
|
+
[2024,"CA","52","SAN FRANCISCO-OAKLAND-BERKELEY (MARIN CNTY)",1.194],
|
|
27
|
+
[2024,"CA","65","SAN JOSE-SUNNYVALE-SANTA CLARA (SAN BENITO CNTY)",1.207],
|
|
28
|
+
[2024,"CA","73","SAN LUIS OBISPO-PASO ROBLES",1.026],
|
|
29
|
+
[2024,"CA","09","SAN JOSE-SUNNYVALE-SANTA CLARA (SANTA CLARA CNTY)",1.207],
|
|
30
|
+
[2024,"CA","66","SANTA CRUZ-WATSONVILLE",1.062],
|
|
31
|
+
[2024,"CA","74","SANTA MARIA-SANTA BARBARA",1.049],
|
|
32
|
+
[2024,"CA","67","SANTA ROSA-PETALUMA",1.078],
|
|
33
|
+
[2024,"CA","68","STOCKTON",1.008],
|
|
34
|
+
[2024,"CA","53","VALLEJO",1.129],
|
|
35
|
+
[2024,"CA","69","VISALIA",1.008],
|
|
36
|
+
[2024,"CA","70","YUBA CITY",1.008],
|
|
37
|
+
[2024,"CA","75","REST OF CALIFORNIA",1.008],
|
|
38
|
+
[2024,"CO","01","COLORADO",0.986],
|
|
39
|
+
[2024,"CT","00","CONNECTICUT",1.011],
|
|
40
|
+
[2024,"DC","01","DC + MD/VA SUBURBS",1.074],
|
|
41
|
+
[2024,"DE","01","DELAWARE",0.959],
|
|
42
|
+
[2024,"FL","03","FORT LAUDERDALE",0.957],
|
|
43
|
+
[2024,"FL","04","MIAMI",0.971],
|
|
44
|
+
[2024,"FL","99","REST OF FLORIDA",0.931],
|
|
45
|
+
[2024,"GA","01","ATLANTA",0.957],
|
|
46
|
+
[2024,"GA","99","REST OF GEORGIA",0.905],
|
|
47
|
+
[2024,"HI","01","HAWAII, GUAM",1.026],
|
|
48
|
+
[2024,"ID","00","IDAHO",0.916],
|
|
49
|
+
[2024,"IL","16","CHICAGO",0.972],
|
|
50
|
+
[2024,"IL","12","EAST ST. LOUIS",0.921],
|
|
51
|
+
[2024,"IL","15","SUBURBAN CHICAGO",0.984],
|
|
52
|
+
[2024,"IL","99","REST OF ILLINOIS",0.918],
|
|
53
|
+
[2024,"IN","00","INDIANA",0.923],
|
|
54
|
+
[2024,"IA","00","IOWA",0.919],
|
|
55
|
+
[2024,"KS","00","KANSAS",0.915],
|
|
56
|
+
[2024,"KY","00","KENTUCKY",0.902],
|
|
57
|
+
[2024,"LA","01","NEW ORLEANS",0.929],
|
|
58
|
+
[2024,"LA","99","REST OF LOUISIANA",0.904],
|
|
59
|
+
[2024,"ME","03","SOUTHERN MAINE",0.964],
|
|
60
|
+
[2024,"ME","99","REST OF MAINE",0.919],
|
|
61
|
+
[2024,"MD","01","BALTIMORE/SURR. CNTYS",1.004],
|
|
62
|
+
[2024,"MD","99","REST OF MARYLAND",0.972],
|
|
63
|
+
[2024,"MA","01","METROPOLITAN BOSTON",1.069],
|
|
64
|
+
[2024,"MA","99","REST OF MASSACHUSETTS",0.995],
|
|
65
|
+
[2024,"MI","01","DETROIT",0.953],
|
|
66
|
+
[2024,"MI","99","REST OF MICHIGAN",0.918],
|
|
67
|
+
[2024,"MN","00","MINNESOTA",0.97],
|
|
68
|
+
[2024,"MS","00","MISSISSIPPI",0.891],
|
|
69
|
+
[2024,"MO","02","METROPOLITAN KANSAS CITY",0.935],
|
|
70
|
+
[2024,"MO","01","METROPOLITAN ST. LOUIS",0.936],
|
|
71
|
+
[2024,"MO","99","REST OF MISSOURI",0.894],
|
|
72
|
+
[2024,"MT","01","MONTANA",0.958],
|
|
73
|
+
[2024,"NE","00","NEBRASKA",0.92],
|
|
74
|
+
[2024,"NV","00","NEVADA",0.958],
|
|
75
|
+
[2024,"NH","40","NEW HAMPSHIRE",0.974],
|
|
76
|
+
[2024,"NJ","01","NORTHERN NJ",1.069],
|
|
77
|
+
[2024,"NJ","99","REST OF NEW JERSEY",1.028],
|
|
78
|
+
[2024,"NM","05","NEW MEXICO",0.916],
|
|
79
|
+
[2024,"NY","01","MANHATTAN",1.067],
|
|
80
|
+
[2024,"NY","02","NYC SUBURBS/LONG ISLAND",1.082],
|
|
81
|
+
[2024,"NY","03","POUGHKPSIE/N NYC SUBURBS",1.03],
|
|
82
|
+
[2024,"NY","04","QUEENS",1.08],
|
|
83
|
+
[2024,"NY","99","REST OF NEW YORK",0.935],
|
|
84
|
+
[2024,"NC","00","NORTH CAROLINA",0.925],
|
|
85
|
+
[2024,"ND","01","NORTH DAKOTA",0.958],
|
|
86
|
+
[2024,"OH","00","OHIO",0.918],
|
|
87
|
+
[2024,"OK","00","OKLAHOMA",0.909],
|
|
88
|
+
[2024,"OR","01","PORTLAND",1.012],
|
|
89
|
+
[2024,"OR","99","REST OF OREGON",0.952],
|
|
90
|
+
[2024,"PA","01","METROPOLITAN PHILADELPHIA",0.995],
|
|
91
|
+
[2024,"PA","99","REST OF PENNSYLVANIA",0.925],
|
|
92
|
+
[2024,"PR","20","PUERTO RICO",0.962],
|
|
93
|
+
[2024,"RI","01","RHODE ISLAND",0.989],
|
|
94
|
+
[2024,"SC","01","SOUTH CAROLINA",0.919],
|
|
95
|
+
[2024,"SD","02","SOUTH DAKOTA",0.958],
|
|
96
|
+
[2024,"TN","35","TENNESSEE",0.911],
|
|
97
|
+
[2024,"TX","31","AUSTIN",0.979],
|
|
98
|
+
[2024,"TX","20","BEAUMONT",0.914],
|
|
99
|
+
[2024,"TX","09","BRAZORIA",0.968],
|
|
100
|
+
[2024,"TX","11","DALLAS",0.967],
|
|
101
|
+
[2024,"TX","28","FORT WORTH",0.963],
|
|
102
|
+
[2024,"TX","15","GALVESTON",0.965],
|
|
103
|
+
[2024,"TX","18","HOUSTON",0.967],
|
|
104
|
+
[2024,"TX","99","REST OF TEXAS",0.933],
|
|
105
|
+
[2024,"UT","09","UTAH",0.928],
|
|
106
|
+
[2024,"VT","50","VERMONT",0.955],
|
|
107
|
+
[2024,"VA","00","VIRGINIA",0.952],
|
|
108
|
+
[2024,"VI","50","VIRGIN ISLANDS",0.962],
|
|
109
|
+
[2024,"WA","02","SEATTLE (KING CNTY)",1.08],
|
|
110
|
+
[2024,"WA","99","REST OF WASHINGTON",0.98],
|
|
111
|
+
[2024,"WV","16","WEST VIRGINIA",0.895],
|
|
112
|
+
[2024,"WI","00","WISCONSIN",0.939],
|
|
113
|
+
[2024,"WY","21","WYOMING",0.958],
|
|
114
|
+
[2025,"AL","00","ALABAMA",0.939],
|
|
115
|
+
[2025,"AK","01","ALASKA",1.304],
|
|
116
|
+
[2025,"AZ","00","ARIZONA",0.988],
|
|
117
|
+
[2025,"AR","13","ARKANSAS",0.934],
|
|
118
|
+
[2025,"CA","54","BAKERSFIELD",1.053],
|
|
119
|
+
[2025,"CA","55","CHICO",1.051],
|
|
120
|
+
[2025,"CA","71","EL CENTRO",1.051],
|
|
121
|
+
[2025,"CA","56","FRESNO",1.051],
|
|
122
|
+
[2025,"CA","57","HANFORD-CORCORAN",1.051],
|
|
123
|
+
[2025,"CA","18","LOS ANGELES-LONG BEACH-ANAHEIM (LOS ANGELES/ORANGE CNTY)",1.113],
|
|
124
|
+
[2025,"CA","58","MADERA",1.051],
|
|
125
|
+
[2025,"CA","59","MERCED",1.051],
|
|
126
|
+
[2025,"CA","60","MODESTO",1.051],
|
|
127
|
+
[2025,"CA","51","NAPA",1.176],
|
|
128
|
+
[2025,"CA","17","OXNARD-THOUSAND OAKS-VENTURA",1.1],
|
|
129
|
+
[2025,"CA","61","REDDING",1.051],
|
|
130
|
+
[2025,"CA","62","RIVERSIDE-SAN BERNARDINO-ONTARIO",1.051],
|
|
131
|
+
[2025,"CA","63","SACRAMENTO-ROSEVILLE-FOLSOM",1.091],
|
|
132
|
+
[2025,"CA","64","SALINAS",1.096],
|
|
133
|
+
[2025,"CA","72","SAN DIEGO-CHULA VISTA-CARLSBAD",1.104],
|
|
134
|
+
[2025,"CA","05","SAN FRANCISCO-OAKLAND-BERKELEY (SAN FRANCISCO/SAN MATEO/ALAMEDA/CONTRA COSTA CNTY)",1.243],
|
|
135
|
+
[2025,"CA","52","SAN FRANCISCO-OAKLAND-BERKELEY (MARIN CNTY)",1.243],
|
|
136
|
+
[2025,"CA","65","SAN JOSE-SUNNYVALE-SANTA CLARA (SAN BENITO CNTY)",1.257],
|
|
137
|
+
[2025,"CA","73","SAN LUIS OBISPO-PASO ROBLES",1.069],
|
|
138
|
+
[2025,"CA","09","SAN JOSE-SUNNYVALE-SANTA CLARA (SANTA CLARA CNTY)",1.257],
|
|
139
|
+
[2025,"CA","66","SANTA CRUZ-WATSONVILLE",1.107],
|
|
140
|
+
[2025,"CA","74","SANTA MARIA-SANTA BARBARA",1.094],
|
|
141
|
+
[2025,"CA","67","SANTA ROSA-PETALUMA",1.123],
|
|
142
|
+
[2025,"CA","68","STOCKTON",1.051],
|
|
143
|
+
[2025,"CA","53","VALLEJO",1.176],
|
|
144
|
+
[2025,"CA","69","VISALIA",1.051],
|
|
145
|
+
[2025,"CA","70","YUBA CITY",1.051],
|
|
146
|
+
[2025,"CA","75","REST OF CALIFORNIA",1.051],
|
|
147
|
+
[2025,"CO","01","COLORADO",1.029],
|
|
148
|
+
[2025,"CT","00","CONNECTICUT",1.054],
|
|
149
|
+
[2025,"DC","01","DC + MD/VA SUBURBS",1.12],
|
|
150
|
+
[2025,"DE","01","DELAWARE",1.001],
|
|
151
|
+
[2025,"FL","03","FORT LAUDERDALE",0.999],
|
|
152
|
+
[2025,"FL","04","MIAMI",1.013],
|
|
153
|
+
[2025,"FL","99","REST OF FLORIDA",0.972],
|
|
154
|
+
[2025,"GA","01","ATLANTA",0.999],
|
|
155
|
+
[2025,"GA","99","REST OF GEORGIA",0.945],
|
|
156
|
+
[2025,"HI","01","HAWAII, GUAM",1.07],
|
|
157
|
+
[2025,"ID","00","IDAHO",0.957],
|
|
158
|
+
[2025,"IL","16","CHICAGO",1.014],
|
|
159
|
+
[2025,"IL","12","EAST ST. LOUIS",0.962],
|
|
160
|
+
[2025,"IL","15","SUBURBAN CHICAGO",1.026],
|
|
161
|
+
[2025,"IL","99","REST OF ILLINOIS",0.959],
|
|
162
|
+
[2025,"IN","00","INDIANA",0.963],
|
|
163
|
+
[2025,"IA","00","IOWA",0.959],
|
|
164
|
+
[2025,"KS","00","KANSAS",0.956],
|
|
165
|
+
[2025,"KY","00","KENTUCKY",0.942],
|
|
166
|
+
[2025,"LA","01","NEW ORLEANS",0.97],
|
|
167
|
+
[2025,"LA","99","REST OF LOUISIANA",0.944],
|
|
168
|
+
[2025,"ME","03","SOUTHERN MAINE",1.006],
|
|
169
|
+
[2025,"ME","99","REST OF MAINE",0.959],
|
|
170
|
+
[2025,"MD","01","BALTIMORE/SURR. CNTYS",1.047],
|
|
171
|
+
[2025,"MD","99","REST OF MARYLAND",1.014],
|
|
172
|
+
[2025,"MA","01","METROPOLITAN BOSTON",1.115],
|
|
173
|
+
[2025,"MA","99","REST OF MASSACHUSETTS",1.038],
|
|
174
|
+
[2025,"MI","01","DETROIT",0.995],
|
|
175
|
+
[2025,"MI","99","REST OF MICHIGAN",0.958],
|
|
176
|
+
[2025,"MN","00","MINNESOTA",1.012],
|
|
177
|
+
[2025,"MS","00","MISSISSIPPI",0.931],
|
|
178
|
+
[2025,"MO","02","METROPOLITAN KANSAS CITY",0.976],
|
|
179
|
+
[2025,"MO","01","METROPOLITAN ST. LOUIS",0.978],
|
|
180
|
+
[2025,"MO","99","REST OF MISSOURI",0.934],
|
|
181
|
+
[2025,"MT","01","MONTANA",1],
|
|
182
|
+
[2025,"NE","00","NEBRASKA",0.961],
|
|
183
|
+
[2025,"NV","00","NEVADA",1],
|
|
184
|
+
[2025,"NH","40","NEW HAMPSHIRE",1.016],
|
|
185
|
+
[2025,"NJ","01","NORTHERN NJ",1.115],
|
|
186
|
+
[2025,"NJ","99","REST OF NEW JERSEY",1.072],
|
|
187
|
+
[2025,"NM","05","NEW MEXICO",0.957],
|
|
188
|
+
[2025,"NY","01","MANHATTAN",1.112],
|
|
189
|
+
[2025,"NY","02","NYC SUBURBS/LONG ISLAND",1.128],
|
|
190
|
+
[2025,"NY","03","POUGHKPSIE/N NYC SUBURBS",1.074],
|
|
191
|
+
[2025,"NY","04","QUEENS",1.126],
|
|
192
|
+
[2025,"NY","99","REST OF NEW YORK",0.976],
|
|
193
|
+
[2025,"NC","00","NORTH CAROLINA",0.965],
|
|
194
|
+
[2025,"ND","01","NORTH DAKOTA",1],
|
|
195
|
+
[2025,"OH","00","OHIO",0.958],
|
|
196
|
+
[2025,"OK","00","OKLAHOMA",0.949],
|
|
197
|
+
[2025,"OR","01","PORTLAND",1.055],
|
|
198
|
+
[2025,"OR","99","REST OF OREGON",0.993],
|
|
199
|
+
[2025,"PA","01","METROPOLITAN PHILADELPHIA",1.038],
|
|
200
|
+
[2025,"PA","99","REST OF PENNSYLVANIA",0.966],
|
|
201
|
+
[2025,"PR","20","PUERTO RICO",1.003],
|
|
202
|
+
[2025,"RI","01","RHODE ISLAND",1.032],
|
|
203
|
+
[2025,"SC","01","SOUTH CAROLINA",0.959],
|
|
204
|
+
[2025,"SD","02","SOUTH DAKOTA",1],
|
|
205
|
+
[2025,"TN","35","TENNESSEE",0.951],
|
|
206
|
+
[2025,"TX","31","AUSTIN",1.022],
|
|
207
|
+
[2025,"TX","20","BEAUMONT",0.955],
|
|
208
|
+
[2025,"TX","09","BRAZORIA",1.01],
|
|
209
|
+
[2025,"TX","11","DALLAS",1.009],
|
|
210
|
+
[2025,"TX","28","FORT WORTH",1.005],
|
|
211
|
+
[2025,"TX","15","GALVESTON",1.007],
|
|
212
|
+
[2025,"TX","18","HOUSTON",1.009],
|
|
213
|
+
[2025,"TX","99","REST OF TEXAS",0.974],
|
|
214
|
+
[2025,"UT","09","UTAH",0.969],
|
|
215
|
+
[2025,"VT","50","VERMONT",0.997],
|
|
216
|
+
[2025,"VA","00","VIRGINIA",0.994],
|
|
217
|
+
[2025,"VI","50","VIRGIN ISLANDS",1.003],
|
|
218
|
+
[2025,"WA","02","SEATTLE (KING CNTY)",1.126],
|
|
219
|
+
[2025,"WA","99","REST OF WASHINGTON",1.022],
|
|
220
|
+
[2025,"WV","16","WEST VIRGINIA",0.935],
|
|
221
|
+
[2025,"WI","00","WISCONSIN",0.98],
|
|
222
|
+
[2025,"WY","21","WYOMING",1],
|
|
223
|
+
[2026,"AL","00","ALABAMA",0.94],
|
|
224
|
+
[2026,"AK","01","ALASKA",1.29],
|
|
225
|
+
[2026,"AZ","00","ARIZONA",0.985],
|
|
226
|
+
[2026,"AR","13","ARKANSAS",0.932],
|
|
227
|
+
[2026,"CA","54","BAKERSFIELD",1.056],
|
|
228
|
+
[2026,"CA","55","CHICO",1.055],
|
|
229
|
+
[2026,"CA","71","EL CENTRO",1.055],
|
|
230
|
+
[2026,"CA","56","FRESNO",1.055],
|
|
231
|
+
[2026,"CA","57","HANFORD-CORCORAN",1.055],
|
|
232
|
+
[2026,"CA","18","LOS ANGELES-LONG BEACH-ANAHEIM (LOS ANGELES/ORANGE CNTY)",1.109],
|
|
233
|
+
[2026,"CA","58","MADERA",1.055],
|
|
234
|
+
[2026,"CA","59","MERCED",1.055],
|
|
235
|
+
[2026,"CA","60","MODESTO",1.055],
|
|
236
|
+
[2026,"CA","51","NAPA",1.186],
|
|
237
|
+
[2026,"CA","17","OXNARD-THOUSAND OAKS-VENTURA",1.102],
|
|
238
|
+
[2026,"CA","61","REDDING",1.055],
|
|
239
|
+
[2026,"CA","62","RIVERSIDE-SAN BERNARDINO-ONTARIO",1.056],
|
|
240
|
+
[2026,"CA","63","SACRAMENTO-ROSEVILLE-FOLSOM",1.097],
|
|
241
|
+
[2026,"CA","64","SALINAS",1.093],
|
|
242
|
+
[2026,"CA","72","SAN DIEGO-CHULA VISTA-CARLSBAD",1.11],
|
|
243
|
+
[2026,"CA","05","SAN FRANCISCO-OAKLAND-BERKELEY (SAN FRANCISCO/SAN MATEO/ALAMEDA/CONTRA COSTA CNTY)",1.247],
|
|
244
|
+
[2026,"CA","52","SAN FRANCISCO-OAKLAND-BERKELEY (MARIN CNTY)",1.247],
|
|
245
|
+
[2026,"CA","65","SAN JOSE-SUNNYVALE-SANTA CLARA (SAN BENITO CNTY)",1.27],
|
|
246
|
+
[2026,"CA","73","SAN LUIS OBISPO-PASO ROBLES",1.076],
|
|
247
|
+
[2026,"CA","09","SAN JOSE-SUNNYVALE-SANTA CLARA (SANTA CLARA CNTY)",1.27],
|
|
248
|
+
[2026,"CA","66","SANTA CRUZ-WATSONVILLE",1.114],
|
|
249
|
+
[2026,"CA","74","SANTA MARIA-SANTA BARBARA",1.094],
|
|
250
|
+
[2026,"CA","67","SANTA ROSA-PETALUMA",1.125],
|
|
251
|
+
[2026,"CA","68","STOCKTON",1.055],
|
|
252
|
+
[2026,"CA","53","VALLEJO",1.186],
|
|
253
|
+
[2026,"CA","69","VISALIA",1.055],
|
|
254
|
+
[2026,"CA","70","YUBA CITY",1.055],
|
|
255
|
+
[2026,"CA","75","REST OF CALIFORNIA",1.055],
|
|
256
|
+
[2026,"CO","01","COLORADO",1.037],
|
|
257
|
+
[2026,"CT","00","CONNECTICUT",1.047],
|
|
258
|
+
[2026,"DC","01","DC + MD/VA SUBURBS",1.114],
|
|
259
|
+
[2026,"DE","01","DELAWARE",0.997],
|
|
260
|
+
[2026,"FL","03","FORT LAUDERDALE",1.006],
|
|
261
|
+
[2026,"FL","04","MIAMI",1.02],
|
|
262
|
+
[2026,"FL","99","REST OF FLORIDA",0.979],
|
|
263
|
+
[2026,"GA","01","ATLANTA",1.009],
|
|
264
|
+
[2026,"GA","99","REST OF GEORGIA",0.948],
|
|
265
|
+
[2026,"HI","01","HAWAII, GUAM",1.066],
|
|
266
|
+
[2026,"ID","00","IDAHO",0.961],
|
|
267
|
+
[2026,"IL","16","CHICAGO",1.006],
|
|
268
|
+
[2026,"IL","12","EAST ST. LOUIS",0.961],
|
|
269
|
+
[2026,"IL","15","SUBURBAN CHICAGO",1.017],
|
|
270
|
+
[2026,"IL","99","REST OF ILLINOIS",0.958],
|
|
271
|
+
[2026,"IN","00","INDIANA",0.965],
|
|
272
|
+
[2026,"IA","00","IOWA",0.959],
|
|
273
|
+
[2026,"KS","00","KANSAS",0.954],
|
|
274
|
+
[2026,"KY","00","KENTUCKY",0.947],
|
|
275
|
+
[2026,"LA","01","NEW ORLEANS",0.972],
|
|
276
|
+
[2026,"LA","99","REST OF LOUISIANA",0.945],
|
|
277
|
+
[2026,"ME","03","SOUTHERN MAINE",0.996],
|
|
278
|
+
[2026,"ME","99","REST OF MAINE",0.961],
|
|
279
|
+
[2026,"MD","01","BALTIMORE/SURR. CNTYS",1.043],
|
|
280
|
+
[2026,"MD","99","REST OF MARYLAND",1.011],
|
|
281
|
+
[2026,"MA","01","METROPOLITAN BOSTON",1.115],
|
|
282
|
+
[2026,"MA","99","REST OF MASSACHUSETTS",1.034],
|
|
283
|
+
[2026,"MI","01","DETROIT",0.983],
|
|
284
|
+
[2026,"MI","99","REST OF MICHIGAN",0.958],
|
|
285
|
+
[2026,"MN","00","MINNESOTA",1.014],
|
|
286
|
+
[2026,"MS","00","MISSISSIPPI",0.933],
|
|
287
|
+
[2026,"MO","02","METROPOLITAN KANSAS CITY",0.971],
|
|
288
|
+
[2026,"MO","01","METROPOLITAN ST. LOUIS",0.977],
|
|
289
|
+
[2026,"MO","99","REST OF MISSOURI",0.934],
|
|
290
|
+
[2026,"MT","01","MONTANA",1],
|
|
291
|
+
[2026,"NE","00","NEBRASKA",0.963],
|
|
292
|
+
[2026,"NV","00","NEVADA",1],
|
|
293
|
+
[2026,"NH","40","NEW HAMPSHIRE",1.02],
|
|
294
|
+
[2026,"NJ","01","NORTHERN NJ",1.11],
|
|
295
|
+
[2026,"NJ","99","REST OF NEW JERSEY",1.065],
|
|
296
|
+
[2026,"NM","05","NEW MEXICO",0.96],
|
|
297
|
+
[2026,"NY","01","MANHATTAN",1.111],
|
|
298
|
+
[2026,"NY","02","NYC SUBURBS/LONG ISLAND",1.124],
|
|
299
|
+
[2026,"NY","03","POUGHKPSIE/N NYC SUBURBS",1.069],
|
|
300
|
+
[2026,"NY","04","QUEENS",1.121],
|
|
301
|
+
[2026,"NY","99","REST OF NEW YORK",0.976],
|
|
302
|
+
[2026,"NC","00","NORTH CAROLINA",0.968],
|
|
303
|
+
[2026,"ND","01","NORTH DAKOTA",1],
|
|
304
|
+
[2026,"OH","00","OHIO",0.958],
|
|
305
|
+
[2026,"OK","00","OKLAHOMA",0.948],
|
|
306
|
+
[2026,"OR","01","PORTLAND",1.062],
|
|
307
|
+
[2026,"OR","99","REST OF OREGON",0.998],
|
|
308
|
+
[2026,"PA","01","METROPOLITAN PHILADELPHIA",1.029],
|
|
309
|
+
[2026,"PA","99","REST OF PENNSYLVANIA",0.96],
|
|
310
|
+
[2026,"PR","20","PUERTO RICO",1.005],
|
|
311
|
+
[2026,"RI","01","RHODE ISLAND",1.026],
|
|
312
|
+
[2026,"SC","01","SOUTH CAROLINA",0.963],
|
|
313
|
+
[2026,"SD","02","SOUTH DAKOTA",1],
|
|
314
|
+
[2026,"TN","35","TENNESSEE",0.956],
|
|
315
|
+
[2026,"TX","31","AUSTIN",1.029],
|
|
316
|
+
[2026,"TX","20","BEAUMONT",0.957],
|
|
317
|
+
[2026,"TX","09","BRAZORIA",1],
|
|
318
|
+
[2026,"TX","11","DALLAS",1.003],
|
|
319
|
+
[2026,"TX","28","FORT WORTH",0.998],
|
|
320
|
+
[2026,"TX","15","GALVESTON",1.001],
|
|
321
|
+
[2026,"TX","18","HOUSTON",1.001],
|
|
322
|
+
[2026,"TX","99","REST OF TEXAS",0.975],
|
|
323
|
+
[2026,"UT","09","UTAH",0.971],
|
|
324
|
+
[2026,"VT","50","VERMONT",0.995],
|
|
325
|
+
[2026,"VA","00","VIRGINIA",0.992],
|
|
326
|
+
[2026,"VI","50","VIRGIN ISLANDS",1.005],
|
|
327
|
+
[2026,"WA","02","SEATTLE (KING CNTY)",1.135],
|
|
328
|
+
[2026,"WA","99","REST OF WASHINGTON",1.032],
|
|
329
|
+
[2026,"WV","16","WEST VIRGINIA",0.937],
|
|
330
|
+
[2026,"WI","00","WISCONSIN",0.98],
|
|
331
|
+
[2026,"WY","21","WYOMING",1],
|
|
332
|
+
];
|
package/src/index.ts
CHANGED
|
@@ -295,12 +295,28 @@ async function httpErrorMessage(res: Response, name: string): Promise<string> {
|
|
|
295
295
|
// internal-host-class.ts; no-op for every third-party upstream, which is why
|
|
296
296
|
// this touches 481 packs' error text and changes none of it.
|
|
297
297
|
return markInternalOrigin(
|
|
298
|
-
`${name}: ${res.status}${detailSuffix(await readDetail(res))}`,
|
|
298
|
+
`${name}: ${res.status}${detailSuffix(await readDetail(res))}${responseHints(res)}`,
|
|
299
299
|
res.url,
|
|
300
300
|
res.status,
|
|
301
301
|
);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Facts about a FAILED response that live in its headers, not its body, appended
|
|
306
|
+
* so the gateway's classifier (which only ever sees this string) can tell a limit
|
|
307
|
+
* from a wall from a refusal (fleet #3477). Both are standard, both are absent on
|
|
308
|
+
* almost every response, and neither is invented: `Retry-After` is the server
|
|
309
|
+
* saying "come back later" (the one thing that makes a 403 a throttle), and
|
|
310
|
+
* Cloudflare marks a bot challenge with `cf-mitigated: challenge`.
|
|
311
|
+
*/
|
|
312
|
+
function responseHints(res: Response): string {
|
|
313
|
+
const hints: string[] = [];
|
|
314
|
+
const retryAfter = res.headers.get('retry-after');
|
|
315
|
+
if (retryAfter) hints.push(`retry-after: ${retryAfter.slice(0, 40)}`);
|
|
316
|
+
if ((res.headers.get('cf-mitigated') ?? '').toLowerCase() === 'challenge') hints.push('Cloudflare bot challenge (cf-mitigated: challenge)');
|
|
317
|
+
return hints.length ? ` (${hints.join('; ')})` : '';
|
|
318
|
+
}
|
|
319
|
+
|
|
304
320
|
/**
|
|
305
321
|
* Just the upstream's own explanation — no name, no status.
|
|
306
322
|
*
|
|
@@ -670,6 +686,254 @@ function pickMessage(node: unknown, depth: number): string | null {
|
|
|
670
686
|
function collapse(s: string): string {
|
|
671
687
|
return s.replace(/\s+/g, ' ').trim();
|
|
672
688
|
}
|
|
689
|
+
|
|
690
|
+
// ── Bounded readers — peak memory, not just wall time (fleet #3322) ──────
|
|
691
|
+
//
|
|
692
|
+
// `parseJson` above does `await res.text()` then `JSON.parse(raw)`, so a
|
|
693
|
+
// multi-megabyte upstream body is held TWICE at peak: once as the raw string
|
|
694
|
+
// and once as the object graph (which for an array of small objects runs
|
|
695
|
+
// several times the byte size). Nothing in `workers/gateway` can see that
|
|
696
|
+
// peak — it happens inside the pack, before any gateway code holds a payload —
|
|
697
|
+
// which is why the gateway-side payload caps of #3293 and #3320 did not move
|
|
698
|
+
// the 503/1102 rate at all.
|
|
699
|
+
//
|
|
700
|
+
// Measured on the live gateway, 2026-10-19, sha fa5de5c34, direct tools/call:
|
|
701
|
+
// statcan_list_cubes 8,049,566 B <- one upstream array, no limit argument
|
|
702
|
+
// gnomad_gene BRCA1 2,732,706 B
|
|
703
|
+
// Five of those in flight at the quality runner's width is ~40 MB of parse
|
|
704
|
+
// peak against a 128 MB isolate; Cloudflare kills the isolate and every
|
|
705
|
+
// request in flight returns 503/1102, which is why the victims look random.
|
|
706
|
+
//
|
|
707
|
+
// `readJsonArrayPrefix` is the fix for the dominant shape: an upstream that
|
|
708
|
+
// returns one enormous top-level JSON array with no server-side paging. It
|
|
709
|
+
// parses elements off the stream and DISCARDS the consumed bytes, so peak
|
|
710
|
+
// retained memory is one element plus one chunk rather than the whole body,
|
|
711
|
+
// and it cancels the body as soon as it has what it was asked for.
|
|
712
|
+
|
|
713
|
+
/** Default ceiling for a single upstream JSON body read into an isolate.
|
|
714
|
+
* Chosen so the known tail (statcan 8.0 MB, gnomad 2.7 MB) is bounded well
|
|
715
|
+
* below the point where a handful of concurrent reads exhausts 128 MB. */
|
|
716
|
+
const DEFAULT_MAX_JSON_BYTES = 1_500_000;
|
|
717
|
+
|
|
718
|
+
interface JsonArrayPrefix<T> {
|
|
719
|
+
/** The decoded elements, at most `maxItems` of them. */
|
|
720
|
+
items: T[];
|
|
721
|
+
/** True when the upstream array had more elements than we read. */
|
|
722
|
+
truncated: boolean;
|
|
723
|
+
/** Bytes actually pulled off the socket — the number that bounds memory. */
|
|
724
|
+
bytesRead: number;
|
|
725
|
+
/** `Content-Length` if the upstream declared one, else null. */
|
|
726
|
+
declaredBytes: number | null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Read the FIRST `maxItems` elements of a top-level JSON array off a response
|
|
731
|
+
* stream without ever materialising the whole body.
|
|
732
|
+
*
|
|
733
|
+
* Use this instead of `parseJson` when the upstream returns one big array and
|
|
734
|
+
* offers no `limit`/`offset` of its own. Peak retained memory is one element
|
|
735
|
+
* plus the current chunk; the body is cancelled as soon as `maxItems` is
|
|
736
|
+
* reached, so the remaining megabytes are never transferred at all.
|
|
737
|
+
*
|
|
738
|
+
* const { items, truncated } = await readJsonArrayPrefix<Cube>(
|
|
739
|
+
* res, 'StatCan WDS', { maxItems: limit + offset });
|
|
740
|
+
*
|
|
741
|
+
* `maxBytes` is a second, independent backstop for a pathological element (a
|
|
742
|
+
* single 50 MB object): hitting it stops the read and sets `truncated`, rather
|
|
743
|
+
* than throwing, because a partial answer plus an explicit truncated flag is
|
|
744
|
+
* more useful to a caller than an error.
|
|
745
|
+
*/
|
|
746
|
+
async function readJsonArrayPrefix<T>(
|
|
747
|
+
res: Response,
|
|
748
|
+
name: string,
|
|
749
|
+
opts: { maxItems: number; maxBytes?: number },
|
|
750
|
+
): Promise<JsonArrayPrefix<T>> {
|
|
751
|
+
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_JSON_BYTES;
|
|
752
|
+
const declaredRaw = Number(res.headers.get('content-length'));
|
|
753
|
+
const declaredBytes = Number.isFinite(declaredRaw) && declaredRaw > 0 ? declaredRaw : null;
|
|
754
|
+
|
|
755
|
+
// No streaming body available (a mock, or an already-buffered response):
|
|
756
|
+
// fall back to the whole-text path so behaviour stays correct, just unbounded.
|
|
757
|
+
if (!res.body) {
|
|
758
|
+
const all = await parseJson<T[]>(res, name);
|
|
759
|
+
if (!Array.isArray(all)) throw new Error(`upstream_down: ${name} returned ${typeof all} where a JSON array was expected.`);
|
|
760
|
+
return {
|
|
761
|
+
items: all.slice(0, opts.maxItems),
|
|
762
|
+
truncated: all.length > opts.maxItems,
|
|
763
|
+
bytesRead: declaredBytes ?? 0,
|
|
764
|
+
declaredBytes,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const reader = res.body.getReader();
|
|
769
|
+
const decoder = new TextDecoder();
|
|
770
|
+
const items: T[] = [];
|
|
771
|
+
|
|
772
|
+
let buf = '';
|
|
773
|
+
let scan = 0;
|
|
774
|
+
let bytesRead = 0;
|
|
775
|
+
let started = false; // consumed the opening '['
|
|
776
|
+
let arrayEnded = false;
|
|
777
|
+
let depth = 0;
|
|
778
|
+
let inStr = false;
|
|
779
|
+
let esc = false;
|
|
780
|
+
let elemStart = -1;
|
|
781
|
+
let truncated = false;
|
|
782
|
+
|
|
783
|
+
const push = (text: string) => {
|
|
784
|
+
const t = text.trim();
|
|
785
|
+
if (!t) return;
|
|
786
|
+
items.push(JSON.parse(t) as T);
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
try {
|
|
790
|
+
outer: while (items.length < opts.maxItems && !arrayEnded) {
|
|
791
|
+
const { value, done } = await reader.read();
|
|
792
|
+
if (done) break;
|
|
793
|
+
bytesRead += value.byteLength;
|
|
794
|
+
buf += decoder.decode(value, { stream: true });
|
|
795
|
+
if (bytesRead > maxBytes) { truncated = true; break; }
|
|
796
|
+
|
|
797
|
+
for (; scan < buf.length; scan++) {
|
|
798
|
+
const ch = buf[scan];
|
|
799
|
+
|
|
800
|
+
if (inStr) {
|
|
801
|
+
if (esc) { esc = false; continue; }
|
|
802
|
+
if (ch === '\\') { esc = true; continue; }
|
|
803
|
+
if (ch === '"') inStr = false;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!started) {
|
|
808
|
+
if (ch === '[') { started = true; continue; }
|
|
809
|
+
if (ch.trim() === '') continue;
|
|
810
|
+
throw new Error(
|
|
811
|
+
`upstream_down: ${name} returned a body that does not begin with a JSON array (saw ${JSON.stringify(ch)}). ` +
|
|
812
|
+
'This reader is only for upstreams that answer with one top-level array.',
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (ch === '"') { if (depth === 0 && elemStart < 0) elemStart = scan; inStr = true; continue; }
|
|
817
|
+
|
|
818
|
+
if (ch === '{' || ch === '[') {
|
|
819
|
+
if (depth === 0 && elemStart < 0) elemStart = scan;
|
|
820
|
+
depth++;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (ch === '}' || ch === ']') {
|
|
825
|
+
if (depth > 0) {
|
|
826
|
+
depth--;
|
|
827
|
+
if (depth === 0 && elemStart >= 0) {
|
|
828
|
+
push(buf.slice(elemStart, scan + 1));
|
|
829
|
+
elemStart = -1;
|
|
830
|
+
if (items.length >= opts.maxItems) { truncated = true; scan++; break outer; }
|
|
831
|
+
}
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
// depth 0 and a closing bracket: the end of the top-level array.
|
|
835
|
+
if (elemStart >= 0) { push(buf.slice(elemStart, scan)); elemStart = -1; }
|
|
836
|
+
arrayEnded = true;
|
|
837
|
+
break outer;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (ch === ',' && depth === 0) {
|
|
841
|
+
if (elemStart >= 0) {
|
|
842
|
+
push(buf.slice(elemStart, scan));
|
|
843
|
+
elemStart = -1;
|
|
844
|
+
if (items.length >= opts.maxItems) { truncated = true; scan++; break outer; }
|
|
845
|
+
}
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// A bare scalar element (number / true / false / null).
|
|
850
|
+
if (depth === 0 && elemStart < 0 && ch.trim() !== '') elemStart = scan;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Drop everything already consumed. THIS is what bounds memory: without
|
|
854
|
+
// it `buf` grows to the full body and the reader is streaming in name only.
|
|
855
|
+
if (elemStart >= 0) {
|
|
856
|
+
buf = buf.slice(elemStart);
|
|
857
|
+
scan -= elemStart;
|
|
858
|
+
elemStart = 0;
|
|
859
|
+
} else {
|
|
860
|
+
buf = buf.slice(scan);
|
|
861
|
+
scan = 0;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
// Cancel rather than drain: the point is that the remaining megabytes are
|
|
866
|
+
// never transferred. A cancel on an already-finished body is a no-op.
|
|
867
|
+
await reader.cancel().catch(() => {});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return { items, truncated, bytesRead, declaredBytes };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* `parseJson`, with a hard ceiling on how many bytes are read.
|
|
875
|
+
*
|
|
876
|
+
* For an upstream whose body is not a single array (so `readJsonArrayPrefix`
|
|
877
|
+
* does not apply) but which can still answer with megabytes. Unlike the array
|
|
878
|
+
* reader this one THROWS when the cap is hit, because a truncated prefix of an
|
|
879
|
+
* arbitrary JSON document is not parseable and there is no partial answer to
|
|
880
|
+
* hand back. A declared `Content-Length` over the cap is refused before a
|
|
881
|
+
* single chunk is read.
|
|
882
|
+
*/
|
|
883
|
+
async function parseJsonBounded<T>(
|
|
884
|
+
res: Response,
|
|
885
|
+
name: string,
|
|
886
|
+
maxBytes: number = DEFAULT_MAX_JSON_BYTES,
|
|
887
|
+
): Promise<T> {
|
|
888
|
+
const declaredRaw = Number(res.headers.get('content-length'));
|
|
889
|
+
const declared = Number.isFinite(declaredRaw) && declaredRaw > 0 ? declaredRaw : null;
|
|
890
|
+
|
|
891
|
+
const tooBig = (n: number) =>
|
|
892
|
+
new Error(
|
|
893
|
+
`upstream_down: ${name} answered with ${n.toLocaleString()} bytes, over this pack's ${maxBytes.toLocaleString()}-byte ceiling. ` +
|
|
894
|
+
'The body is not read past the ceiling on purpose — a few concurrent reads of a body that size exhaust the ' +
|
|
895
|
+
'Worker isolate and take every other in-flight request down with them. Narrow the request (a smaller range, ' +
|
|
896
|
+
'a specific id, or a lower limit) and retry.',
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
if (declared !== null && declared > maxBytes) {
|
|
900
|
+
await res.body?.cancel().catch(() => {});
|
|
901
|
+
throw tooBig(declared);
|
|
902
|
+
}
|
|
903
|
+
if (!res.body) return parseJson<T>(res, name);
|
|
904
|
+
|
|
905
|
+
const reader = res.body.getReader();
|
|
906
|
+
const decoder = new TextDecoder();
|
|
907
|
+
let raw = '';
|
|
908
|
+
let bytesRead = 0;
|
|
909
|
+
try {
|
|
910
|
+
for (;;) {
|
|
911
|
+
const { value, done } = await reader.read();
|
|
912
|
+
if (done) break;
|
|
913
|
+
bytesRead += value.byteLength;
|
|
914
|
+
if (bytesRead > maxBytes) throw tooBig(bytesRead);
|
|
915
|
+
raw += decoder.decode(value, { stream: true });
|
|
916
|
+
}
|
|
917
|
+
raw += decoder.decode();
|
|
918
|
+
} finally {
|
|
919
|
+
await reader.cancel().catch(() => {});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!raw.trim()) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
`upstream_down: ${name} answered HTTP ${res.status} with an EMPTY body where JSON was expected. ` +
|
|
925
|
+
'Nothing about the request can cause this — it is an upstream fault, and the same call may well work on retry.',
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
return JSON.parse(raw) as T;
|
|
930
|
+
} catch {
|
|
931
|
+
throw new Error(
|
|
932
|
+
`upstream_down: ${name} answered HTTP ${res.status} with a body that is not valid JSON. ` +
|
|
933
|
+
`It begins: ${stripMarkup(raw).slice(0, 120) || '(unreadable)'}`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
673
937
|
/**
|
|
674
938
|
* What does Medicare PAY — physician fee schedule, lab rates, and per-day unit
|
|
675
939
|
* limits.
|
|
@@ -737,9 +1001,26 @@ function collapse(s: string): string {
|
|
|
737
1001
|
* year AND release (quarterly-ish: A=Jan primary annual update, B=Apr, C=Jul,
|
|
738
1002
|
* D=Oct as needed) are stated on every response, and an unloaded year/release
|
|
739
1003
|
* returns year_not_loaded rather than silently substituting another vintage.
|
|
1004
|
+
*
|
|
1005
|
+
* OPPS (Hospital Outpatient Prospective Payment System) is the sixth data
|
|
1006
|
+
* source, added for fleet task #3430 — what Medicare pays a HOSPITAL
|
|
1007
|
+
* OUTPATIENT department (as opposed to a physician office, a lab, or a
|
|
1008
|
+
* supplier) per HCPCS code: status indicator, Ambulatory Payment
|
|
1009
|
+
* Classification (APC), relative weight, national payment rate, and
|
|
1010
|
+
* beneficiary copayment, from CMS's quarterly OPPS Addendum B. Like pfs_rvu,
|
|
1011
|
+
* NO code descriptor is stored — Addendum B mixes AMA-copyright CPT codes
|
|
1012
|
+
* with CMS-owned HCPCS Level II codes under one shared descriptor column
|
|
1013
|
+
* with no per-row way to separate them, so none is kept. Device and drug/
|
|
1014
|
+
* biological PASS-THROUGH status (a separate, outside-the-APC payment while
|
|
1015
|
+
* a new device or drug holds that status) is derived from the status
|
|
1016
|
+
* indicator itself (SI='H' device, SI='G' drug/biological) rather than from
|
|
1017
|
+
* CMS's own "pass-through expiration" column, which is unreliable in
|
|
1018
|
+
* practice. Vintage is calendar-quarter (Q1=Jan, Q2=Apr, Q3=Jul, Q4=Oct),
|
|
1019
|
+
* stated on every response.
|
|
740
1020
|
*/
|
|
741
1021
|
|
|
742
1022
|
import { ADLT_DATA_AS_OF, ADLT_SOURCE_URL, SNAPSHOT_LAST_MODIFIED, matchAdlt, type AdltTest } from './adlt.js';
|
|
1023
|
+
import { FQHC_GAF_ROWS } from './fqhc-gaf.js';
|
|
743
1024
|
import { BRAND_DATA_AS_OF, BRAND_SOURCE, BRAND_TESTS, gtrLookup, matchTests, norm as normName } from './brand-codes.js';
|
|
744
1025
|
|
|
745
1026
|
// Bound every fetch() in this pack to a fixed timeout — an upstream that
|
|
@@ -785,6 +1066,14 @@ interface PtpEdit {
|
|
|
785
1066
|
service_type: string; column1: string; column2: string; modifier_indicator: string;
|
|
786
1067
|
status: string; effective_date: string; first_seen_quarter: string; last_seen_quarter: string;
|
|
787
1068
|
}
|
|
1069
|
+
interface OppsRow {
|
|
1070
|
+
year: number; quarter: string; hcpcs: string; status_indicator: string; apc: string | null;
|
|
1071
|
+
relative_weight: number | null; payment_rate_usd: number | null;
|
|
1072
|
+
national_unadjusted_copay_usd: number | null; minimum_unadjusted_copay_usd: number | null;
|
|
1073
|
+
ira_coinsurance_pct: number | null; adjusted_beneficiary_copay_usd: number | null;
|
|
1074
|
+
device_pass_through: boolean; drug_or_biological_pass_through: boolean;
|
|
1075
|
+
note: string | null; changed_this_release: boolean;
|
|
1076
|
+
}
|
|
788
1077
|
|
|
789
1078
|
const PTP_WINDOW_NOTE = 'ncci_ptp_edits holds the FULL NCCI PTP baseline (2026q3, v322r0, effective 2026-07-01) with the 2025q1..2026q3 quarterly change files layered on top — see medicare_pricing_coverage for the loaded vintage.';
|
|
790
1079
|
|
|
@@ -838,6 +1127,20 @@ function resolveState(q: unknown): { abbr: string | null; note: string } {
|
|
|
838
1127
|
if (byName) return { abbr: byName, note: `Resolved "${t}" to ${byName}.` };
|
|
839
1128
|
return { abbr: null, note: `"${t}" is not a recognized US state, DC, or territory (PR/VI). Pass a 2-letter code or full name.` };
|
|
840
1129
|
}
|
|
1130
|
+
// OPPS releases are quarterly and calendar-aligned (Q1=Jan, Q2=Apr, Q3=Jul,
|
|
1131
|
+
// Q4=Oct) — accept a bare quarter ("Q3"/"3") or the month CMS actually names
|
|
1132
|
+
// its release after ("july"/"jul"), since callers are more likely to know
|
|
1133
|
+
// the month a code's rate changed than CMS's internal quarter label.
|
|
1134
|
+
const QUARTER_ALIASES: Record<string, string> = {
|
|
1135
|
+
q1: 'Q1', '1': 'Q1', jan: 'Q1', january: 'Q1',
|
|
1136
|
+
q2: 'Q2', '2': 'Q2', apr: 'Q2', april: 'Q2',
|
|
1137
|
+
q3: 'Q3', '3': 'Q3', jul: 'Q3', july: 'Q3',
|
|
1138
|
+
q4: 'Q4', '4': 'Q4', oct: 'Q4', october: 'Q4',
|
|
1139
|
+
};
|
|
1140
|
+
function resolveQuarter(q: unknown): string | null {
|
|
1141
|
+
if (typeof q !== 'string' || !q.trim()) return null;
|
|
1142
|
+
return QUARTER_ALIASES[q.trim().toLowerCase()] ?? null;
|
|
1143
|
+
}
|
|
841
1144
|
const DME_CATEGORY: Record<string, string> = {
|
|
842
1145
|
IN: 'Inexpensive and Other Routinely Purchased Items', FS: 'Frequently Serviced Items',
|
|
843
1146
|
CR: 'Capped Rental Items', OX: 'Oxygen and Oxygen Equipment',
|
|
@@ -864,6 +1167,43 @@ const STATUS: Record<string, string> = {
|
|
|
864
1167
|
X: 'Statutorily excluded from the fee schedule',
|
|
865
1168
|
};
|
|
866
1169
|
|
|
1170
|
+
// ── OPPS (Hospital Outpatient Prospective Payment System) ────────────────
|
|
1171
|
+
// CMS's own status-indicator legend, stable across years. H and G are the
|
|
1172
|
+
// two that carry PASS-THROUGH status (a separate payment outside the
|
|
1173
|
+
// standard APC amount while a new device/drug holds it) — device_pass_through
|
|
1174
|
+
// and drug_or_biological_pass_through on medicare_opps_rate are derived from
|
|
1175
|
+
// these two codes rather than from CMS's own "pass-through expiration" date
|
|
1176
|
+
// column, which the loaded release does not reliably populate.
|
|
1177
|
+
const OPPS_SI: Record<string, string> = {
|
|
1178
|
+
A: 'Paid under a fee schedule or payment system other than OPPS — not a hospital outpatient payment.',
|
|
1179
|
+
B: 'Not recognized by OPPS on an outpatient claim; another code reports this service.',
|
|
1180
|
+
C: 'Inpatient procedure — not paid under OPPS when billed as outpatient.',
|
|
1181
|
+
D: 'Discontinued code.',
|
|
1182
|
+
E1: 'Not covered by any Medicare outpatient benefit category — not paid under OPPS.',
|
|
1183
|
+
E2: 'No pricing information or claims data available — not paid under OPPS.',
|
|
1184
|
+
F: 'Corneal tissue acquisition, certain CRNA services, or hepatitis B vaccine — paid at reasonable cost, not subject to deductible or coinsurance.',
|
|
1185
|
+
G: 'PASS-THROUGH drug or biological — paid separately, outside the standard APC amount, while it holds pass-through status.',
|
|
1186
|
+
H: 'PASS-THROUGH device category — paid separately, outside the standard APC amount, while the device holds pass-through status.',
|
|
1187
|
+
J1: 'Hospital Part B service paid through a comprehensive APC — packaged with everything else on the claim into one payment.',
|
|
1188
|
+
J2: 'Hospital Part B service that may be paid through a comprehensive APC, depending on what else is billed the same day.',
|
|
1189
|
+
K: 'Non-pass-through drug, biological, or therapeutic radiopharmaceutical — paid separately under OPPS.',
|
|
1190
|
+
L: 'Influenza or pneumococcal pneumonia vaccine — paid at reasonable cost, not subject to deductible or coinsurance.',
|
|
1191
|
+
M: 'Not billable to the Medicare Administrative Contractor under this benefit.',
|
|
1192
|
+
N: 'Packaged into the APC payment for the primary service on the claim — no separate OPPS payment for this code.',
|
|
1193
|
+
P: 'Partial hospitalization.',
|
|
1194
|
+
Q1: 'Conditionally packaged ("STV-packaged") — separately payable only when no other type-S/T/V service is on the same claim.',
|
|
1195
|
+
Q2: 'Conditionally packaged ("T-packaged") — separately payable only when no type-T service is on the same claim.',
|
|
1196
|
+
Q3: 'May be paid through a composite APC with certain other codes billed the same day.',
|
|
1197
|
+
Q4: 'Conditionally packaged laboratory test.',
|
|
1198
|
+
R: 'Blood or blood product — paid separately under OPPS.',
|
|
1199
|
+
S: 'Procedure or service paid separately under OPPS, not subject to multiple-procedure discounting.',
|
|
1200
|
+
T: 'Procedure or service paid separately under OPPS, subject to multiple-procedure discounting when billed with another T-status code the same day.',
|
|
1201
|
+
U: 'Brachytherapy source — paid separately under OPPS.',
|
|
1202
|
+
V: 'Clinic or emergency department visit — paid separately under OPPS.',
|
|
1203
|
+
X: 'Ancillary service — paid separately under OPPS.',
|
|
1204
|
+
Y: 'Non-implantable durable medical equipment — statutorily non-covered on this claim type.',
|
|
1205
|
+
};
|
|
1206
|
+
|
|
867
1207
|
// ── FQHC PPS base payment rate (fleet #3241) ──────────────────────────────
|
|
868
1208
|
//
|
|
869
1209
|
// CMS publishes this ONE number a year, in a CMS Manual System TRANSMITTAL
|
|
@@ -1012,6 +1352,21 @@ const tools: McpToolExport['tools'] = [
|
|
|
1012
1352
|
required: ['code'],
|
|
1013
1353
|
},
|
|
1014
1354
|
},
|
|
1355
|
+
{
|
|
1356
|
+
name: 'medicare_opps_rate',
|
|
1357
|
+
description:
|
|
1358
|
+
'What does Medicare pay a HOSPITAL OUTPATIENT department for a procedure, device, or drug code? Returns the status indicator, Ambulatory Payment Classification (APC), relative weight, national payment rate, and beneficiary copayment for a HCPCS/CPT code under the Hospital Outpatient Prospective Payment System (OPPS) — the schedule that applies when the same service is billed by a hospital\'s outpatient department rather than a physician office (medicare_physician_payment) or a lab (medicare_lab_rate). Answers "what does Medicare pay for CPT 33289 in a hospital outpatient setting", "what is the APC for C2624", "is this device on pass-through status", "what is the OPPS payment rate and copay for this code". '
|
|
1359
|
+
+ 'Flags device_pass_through and drug_or_biological_pass_through — whether the code is CURRENTLY in a CMS pass-through category (status indicator H or G), meaning it is paid separately, outside the normal APC amount, rather than packaged into the procedure it goes with. A packaged code (status indicator N, among others) still has a valid status_indicator but no apc/payment_rate_usd, because OPPS pays for it only as part of whatever procedure it is billed with — that is a normal result, not a missing one. States the fee-schedule YEAR and QUARTER (Q1=Jan, Q2=Apr, Q3=Jul, Q4=Oct) used on every response. Sourced from CMS, OPPS Addendum B.',
|
|
1360
|
+
inputSchema: {
|
|
1361
|
+
type: 'object' as const,
|
|
1362
|
+
properties: {
|
|
1363
|
+
code: { type: 'string', description: 'HCPCS or CPT code, e.g. "33289" (CardioMEMS implant procedure) or "C2624" (implantable wireless pulmonary artery pressure sensor).' },
|
|
1364
|
+
year: { type: 'number', description: 'Calendar year, e.g. 2026. Omit to use the newest loaded year, which is stated in the response.' },
|
|
1365
|
+
quarter: { type: 'string', description: 'Which quarterly release: "Q1".."Q4", or the release month CMS actually uses ("january"/"april"/"july"/"october"). Omit to use the newest loaded quarter for the resolved year.' },
|
|
1366
|
+
},
|
|
1367
|
+
required: ['code'],
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1015
1370
|
{
|
|
1016
1371
|
name: 'medicare_code_units_limit',
|
|
1017
1372
|
description:
|
|
@@ -1059,7 +1414,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1059
1414
|
name: 'medicare_fqhc_pps_rate',
|
|
1060
1415
|
description:
|
|
1061
1416
|
'What does Medicare pay a Federally Qualified Health Center (FQHC) per visit? Returns the CMS FQHC Prospective Payment System (PPS) national BASE payment rate for a calendar year — the figure CMS publishes once a year, before any per-FQHC Geographic Adjustment Factor (GAF) is applied. Answers "what is the FQHC PPS base rate", "how much does Medicare pay an FQHC per encounter", "FQHC PPS rate for 2025 vs 2026". '
|
|
1062
|
-
+ 'Medicare pays an FQHC the LESSER of its actual charges or this PPS rate for all FQHC services furnished to a beneficiary on a day with a qualifying face-to-face visit — it is a per-DAY encounter rate, not a fee per procedure code (unlike medicare_physician_payment/medicare_lab_rate). This tool
|
|
1417
|
+
+ 'Medicare pays an FQHC the LESSER of its actual charges or this PPS rate for all FQHC services furnished to a beneficiary on a day with a qualifying face-to-face visit — it is a per-DAY encounter rate, not a fee per procedure code (unlike medicare_physician_payment/medicare_lab_rate). This tool returns only the national base rate before the geographic adjustment; for the GAF-adjusted amount in a named locality use medicare_fqhc_gaf. Omit year for the newest loaded year plus the full known history (small — CMS publishes one number a year); pass year for one row.',
|
|
1063
1418
|
inputSchema: {
|
|
1064
1419
|
type: 'object' as const,
|
|
1065
1420
|
properties: {
|
|
@@ -1067,6 +1422,35 @@ const tools: McpToolExport['tools'] = [
|
|
|
1067
1422
|
},
|
|
1068
1423
|
},
|
|
1069
1424
|
},
|
|
1425
|
+
{
|
|
1426
|
+
name: 'medicare_gpci_lookup',
|
|
1427
|
+
description:
|
|
1428
|
+
'What are the Medicare geographic adjustment indices (GPCIs) for a place — with NO procedure code needed? Returns the work, practice-expense and malpractice GPCI for a Medicare payment locality (or every locality in a state), for a calendar year. Answers "GPCI for Rest of Florida locality 99", "Medicare geographic practice cost index for San Francisco", "work GPCI Manhattan 2025", "which Medicare localities does California have". '
|
|
1429
|
+
+ 'Medicare pays by LOCALITY, not state (California has nine), so a state or ambiguous name returns every matching locality rather than one chosen quietly, and the resolution is stated. The work GPCI is the floored value CMS pays on (1.0 floor where it applies). Pass state ("FL" or "Florida"), a locality name ("Bakersfield"), and/or locality_number ("99"); year defaults to the newest loaded and is stated. For the FQHC-specific adjustment see medicare_fqhc_gaf; for a dollar payment see medicare_physician_payment. Sourced from the CMS Physician Fee Schedule geographic practice cost indices.',
|
|
1430
|
+
inputSchema: {
|
|
1431
|
+
type: 'object' as const,
|
|
1432
|
+
properties: {
|
|
1433
|
+
locality: { type: 'string', description: 'State code ("FL"), state name ("Florida"), or Medicare locality name ("Bakersfield", "Manhattan", "Rest of Florida").' },
|
|
1434
|
+
state: { type: 'string', description: 'Optional state code or name; combine with locality_number to pin one locality.' },
|
|
1435
|
+
locality_number: { type: 'string', description: 'Optional Medicare locality number, e.g. "99" (Rest of Florida) — needs state or locality to say which state.' },
|
|
1436
|
+
year: { type: 'number', description: 'Calendar year, e.g. 2025. Omit for the newest loaded year (stated in the response).' },
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
name: 'medicare_fqhc_gaf',
|
|
1442
|
+
description:
|
|
1443
|
+
'What is the FQHC Geographic Adjustment Factor (GAF) for a place, and what does Medicare pay a Federally Qualified Health Center per visit THERE? Returns the official CMS FQHC PPS GAF for a Medicare locality and calendar year, and the GAF-adjusted per-visit rate (national base rate x GAF). Answers "FQHC PPS GAF for Orlando, Florida 2025", "CY2026 FQHC geographic adjustment Rest of California", "what is the FQHC PPS rate in Alaska". '
|
|
1444
|
+
+ 'The GAF is CMS\'s own published number per locality (adapted from the work and practice-expense GPCIs, with the 1.0 floor), not something this tool estimates. A state returns all its localities; a city is not a locality, so name the locality or pick from the list (Florida has Fort Lauderdale, Miami and Rest of Florida). Complements medicare_fqhc_pps_rate (national base rate) and medicare_gpci_lookup (the underlying GPCIs). Years 2024-2026 loaded. Sourced from the CMS FQHC PPS geographic adjustment factor tables.',
|
|
1445
|
+
inputSchema: {
|
|
1446
|
+
type: 'object' as const,
|
|
1447
|
+
properties: {
|
|
1448
|
+
locality: { type: 'string', description: 'State code ("FL"), state name ("Florida"), or Medicare locality name ("Fort Lauderdale", "Rest of Florida"). A bare state returns all of its localities.' },
|
|
1449
|
+
locality_number: { type: 'string', description: 'Optional Medicare locality number, e.g. "99"; with a state it pins one locality.' },
|
|
1450
|
+
year: { type: 'number', description: 'Calendar year, 2024-2026. Omit for the newest loaded year (stated in the response).' },
|
|
1451
|
+
},
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1070
1454
|
{
|
|
1071
1455
|
name: 'medicare_pricing_coverage',
|
|
1072
1456
|
description:
|
|
@@ -1141,12 +1525,154 @@ async function resolveLocality(cfg: Cfg, q: unknown, year: number): Promise<{ ch
|
|
|
1141
1525
|
return { chosen, alternatives: others, note };
|
|
1142
1526
|
}
|
|
1143
1527
|
|
|
1528
|
+
|
|
1529
|
+
const GPCI_SOURCE = 'CMS, Physician Fee Schedule geographic practice cost indices (GPCIs) by Medicare locality';
|
|
1530
|
+
const FQHC_GAF_SOURCE = 'CMS, FQHC Prospective Payment System geographic adjustment factors by Medicare locality (with 1.0 floor)';
|
|
1531
|
+
|
|
1532
|
+
/** Split a free-text place into {state?, nameQuery?}. A bare state matches the whole state. */
|
|
1533
|
+
function placeQuery(locality: unknown, state: unknown): { abbr: string | null; name: string | null } {
|
|
1534
|
+
let l = typeof locality === 'string' ? locality.trim() : '';
|
|
1535
|
+
let st = typeof state === 'string' ? state.trim() : '';
|
|
1536
|
+
// "Orlando, Florida" / "Orlando, FL": the part after the last comma is the state.
|
|
1537
|
+
const comma = l.lastIndexOf(',');
|
|
1538
|
+
if (comma > 0 && !st) { st = l.slice(comma + 1).trim(); l = l.slice(0, comma).trim(); }
|
|
1539
|
+
const asState = (t: string) => {
|
|
1540
|
+
if (!t) return null;
|
|
1541
|
+
const u = t.toUpperCase();
|
|
1542
|
+
if (VALID_STATE_ABBRS.has(u)) return u;
|
|
1543
|
+
return STATE_NAME_TO_ABBR[t.toLowerCase()] ?? null;
|
|
1544
|
+
};
|
|
1545
|
+
const stAbbr = asState(st);
|
|
1546
|
+
const lAbbr = asState(l);
|
|
1547
|
+
if (stAbbr) return { abbr: stAbbr, name: lAbbr ? null : (l || null) };
|
|
1548
|
+
if (lAbbr) return { abbr: lAbbr, name: null };
|
|
1549
|
+
return { abbr: null, name: l || null };
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function fqhcGafLookup(args: Record<string, unknown>) {
|
|
1553
|
+
const years = [...new Set(FQHC_GAF_ROWS.map((r) => r[0]))].sort();
|
|
1554
|
+
const askedYear = typeof args.year === 'number' ? Math.floor(args.year) : null;
|
|
1555
|
+
if (askedYear !== null && !years.includes(askedYear)) {
|
|
1556
|
+
return { found: false, reason: 'year_not_loaded', loaded_years: years, hint: `No FQHC GAF table is loaded for ${askedYear}; loaded years: ${years.join(', ')}. GAFs change every year, so another year's value is not substituted.` };
|
|
1557
|
+
}
|
|
1558
|
+
const year = askedYear ?? years[years.length - 1];
|
|
1559
|
+
const { abbr, name } = placeQuery(args.locality, args.state);
|
|
1560
|
+
const num = typeof args.locality_number === 'string' ? args.locality_number.trim().replace(/^0+(?=\d)/, '') : '';
|
|
1561
|
+
if (!abbr && !name) return { found: false, reason: 'no_place', hint: 'Pass locality (state code/name or a Medicare locality name), e.g. {"locality":"Florida","year":2025}.' };
|
|
1562
|
+
let rows = FQHC_GAF_ROWS.filter((r) => r[0] === year);
|
|
1563
|
+
if (abbr) rows = rows.filter((r) => r[1] === abbr);
|
|
1564
|
+
let cityNote = '';
|
|
1565
|
+
if (name) {
|
|
1566
|
+
const n = name.toLowerCase();
|
|
1567
|
+
const named = rows.filter((r) => r[3].toLowerCase().includes(n));
|
|
1568
|
+
if (!named.length && abbr && rows.length) cityNote = `"${name}" is not a Medicare locality name, so every ${abbr} locality is listed — CMS assigns a city to a locality by county; pick the one that contains it. `;
|
|
1569
|
+
else rows = named;
|
|
1570
|
+
}
|
|
1571
|
+
if (num) rows = rows.filter((r) => r[2].replace(/^0+(?=\d)/, '') === num);
|
|
1572
|
+
if (!rows.length) {
|
|
1573
|
+
return { found: false, reason: 'no_locality_match', year, hint: `No Medicare locality matched ${JSON.stringify(args.locality ?? args.state)}${num ? ` number ${num}` : ''} in ${year}. Pass a state ("FL") to list its localities; a city is not a locality.` };
|
|
1574
|
+
}
|
|
1575
|
+
const base = FQHC_PPS_BASE_RATES.find((r) => r.year === year) ?? null;
|
|
1576
|
+
const localities = rows.map((r) => ({
|
|
1577
|
+
state: r[1], locality_number: r[2], locality_name: r[3], fqhc_gaf: r[4],
|
|
1578
|
+
gaf_adjusted_rate_usd: base ? round2(base.base_rate_usd * r[4]) : null,
|
|
1579
|
+
}));
|
|
1580
|
+
return {
|
|
1581
|
+
found: true,
|
|
1582
|
+
year,
|
|
1583
|
+
year_was_defaulted: askedYear === null,
|
|
1584
|
+
localities,
|
|
1585
|
+
locality_count: localities.length,
|
|
1586
|
+
base_rate_usd: base?.base_rate_usd ?? null,
|
|
1587
|
+
formula: base ? `FQHC PPS base rate $${base.base_rate_usd.toFixed(2)} x FQHC GAF = per-visit rate (before the new-patient / IPPE / AWV add-on and the lesser-of-charges rule)` : null,
|
|
1588
|
+
note: localities.length > 1
|
|
1589
|
+
? `${cityNote}${localities.length} Medicare localities matched and their GAFs differ; a city is not a locality — pick the locality that contains the health center.`
|
|
1590
|
+
: `Resolved to ${localities[0].locality_name} (${localities[0].state}, locality ${localities[0].locality_number}).`,
|
|
1591
|
+
source: FQHC_GAF_SOURCE,
|
|
1592
|
+
data_as_of: `CY ${year}`,
|
|
1593
|
+
loaded_years: years,
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
async function gpciLookup(cfg: Cfg, args: Record<string, unknown>) {
|
|
1598
|
+
const loaded = await pg<{ year: number }>(cfg, 'pfs_gpci?select=year&order=year.desc&limit=1000');
|
|
1599
|
+
const years = [...new Set(loaded.map((r) => r.year))].sort();
|
|
1600
|
+
const askedYear = typeof args.year === 'number' ? Math.floor(args.year) : null;
|
|
1601
|
+
if (askedYear !== null && !years.includes(askedYear)) {
|
|
1602
|
+
return { found: false, reason: 'year_not_loaded', loaded_years: years, hint: `No GPCI table is loaded for ${askedYear}; loaded years: ${years.join(', ')}. GPCIs change every year, so another year's value is not substituted.` };
|
|
1603
|
+
}
|
|
1604
|
+
const year = askedYear ?? years[years.length - 1];
|
|
1605
|
+
const { abbr, name } = placeQuery(args.locality, args.state);
|
|
1606
|
+
const num = typeof args.locality_number === 'string' ? args.locality_number.trim() : '';
|
|
1607
|
+
if (!abbr && !name) return { found: false, reason: 'no_place', hint: 'Pass locality (state code/name or a Medicare locality name), e.g. {"locality":"Florida","year":2025}.' };
|
|
1608
|
+
let rows: Gpci[];
|
|
1609
|
+
let cityNote = '';
|
|
1610
|
+
// fleet #3475: every row carries HOW it matched, and the order is decided here
|
|
1611
|
+
// (rank, then locality_number), not inherited from the RPC's similarity sort.
|
|
1612
|
+
// state - the query named a state, this is one of its localities
|
|
1613
|
+
// exact - the locality name equals the query
|
|
1614
|
+
// contains - the locality name contains the query
|
|
1615
|
+
// similar - trigram-similar only (e.g. "REST OF MAINE" for "Rest of Florida")
|
|
1616
|
+
type Match = 'exact' | 'state' | 'contains' | 'similar';
|
|
1617
|
+
const matchOf = new Map<Gpci, Match>();
|
|
1618
|
+
if (abbr) {
|
|
1619
|
+
rows = await pg<Gpci>(cfg, `pfs_gpci?year=eq.${year}&state=eq.${abbr}&select=*&order=locality_number.asc&limit=100`);
|
|
1620
|
+
if (name) {
|
|
1621
|
+
const n = name.toLowerCase();
|
|
1622
|
+
const named = rows.filter((r) => r.locality_name.toLowerCase().includes(n));
|
|
1623
|
+
if (!named.length && rows.length) { cityNote = `"${name}" is not a Medicare locality name, so every ${abbr} locality is listed — CMS assigns a city to a locality by county; pick the one that contains it. `; rows.forEach((r) => matchOf.set(r, 'state')); }
|
|
1624
|
+
else { rows = named; rows.forEach((r) => matchOf.set(r, r.locality_name.toLowerCase() === n ? 'exact' : 'contains')); }
|
|
1625
|
+
} else rows.forEach((r) => matchOf.set(r, 'state'));
|
|
1626
|
+
} else {
|
|
1627
|
+
rows = await rpc<Gpci>(cfg, 'resolve_medicare_locality', { q: name, yr: year });
|
|
1628
|
+
const n = (name ?? '').trim().toLowerCase();
|
|
1629
|
+
rows.forEach((r) => {
|
|
1630
|
+
const ln = r.locality_name.toLowerCase();
|
|
1631
|
+
matchOf.set(r, ln === n ? 'exact' : ln.includes(n) ? 'contains' : 'similar');
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (num) rows = rows.filter((r) => r.locality_number.replace(/^0+(?=\d)/, '') === num.replace(/^0+(?=\d)/, ''));
|
|
1635
|
+
if (!rows.length) return { found: false, reason: 'no_locality_match', year, hint: `No Medicare locality matched ${JSON.stringify(args.locality ?? args.state)}${num ? ` number ${num}` : ''} in ${year}. Pass a state ("FL") to list its localities.` };
|
|
1636
|
+
const rank: Record<Match, number> = { exact: 0, state: 0, contains: 1, similar: 2 };
|
|
1637
|
+
rows = [...rows].sort((a, b) => rank[matchOf.get(a)!] - rank[matchOf.get(b)!] || a.state.localeCompare(b.state) || a.locality_number.localeCompare(b.locality_number));
|
|
1638
|
+
const shape = (r: Gpci) => ({
|
|
1639
|
+
state: r.state, locality_number: r.locality_number, locality_name: r.locality_name,
|
|
1640
|
+
match: matchOf.get(r)!,
|
|
1641
|
+
work_gpci: r.pw_gpci, practice_expense_gpci: r.pe_gpci, malpractice_gpci: r.mp_gpci,
|
|
1642
|
+
});
|
|
1643
|
+
// Near-matches are split out only when something matched the query by name:
|
|
1644
|
+
// a caller who named a locality gets that locality in `localities`, and the
|
|
1645
|
+
// trigram-similar neighbours under a separate key where they cannot be mistaken
|
|
1646
|
+
// for it. With no name match at all, similar rows stay in `localities`, labelled.
|
|
1647
|
+
const strong = rows.filter((r) => matchOf.get(r) !== 'similar');
|
|
1648
|
+
const localities = (strong.length ? strong : rows).slice(0, 40).map(shape);
|
|
1649
|
+
const nearMatches = strong.length ? rows.filter((r) => matchOf.get(r) === 'similar').slice(0, 40).map(shape) : [];
|
|
1650
|
+
return {
|
|
1651
|
+
found: true,
|
|
1652
|
+
year,
|
|
1653
|
+
year_was_defaulted: askedYear === null,
|
|
1654
|
+
localities,
|
|
1655
|
+
locality_count: localities.length,
|
|
1656
|
+
near_matches: nearMatches,
|
|
1657
|
+
near_match_count: nearMatches.length,
|
|
1658
|
+
match_note: 'Each row carries `match`: exact (name equals the query), state (a locality of the state named), contains (name contains the query), similar (spelling-similar only, NOT the place asked for). `near_matches`, when present, holds spelling-similar localities from other places; do not use them as the answer. Rows are ordered exact/state, then contains, then similar, deterministically.',
|
|
1659
|
+
note: localities.length > 1
|
|
1660
|
+
? `${cityNote}${localities.length} Medicare localities matched and they differ; name a locality or pass locality_number to pin one.`
|
|
1661
|
+
: `Resolved to ${localities[0].locality_name} (${localities[0].state}, locality ${localities[0].locality_number}).`,
|
|
1662
|
+
work_gpci_note: 'Work GPCI is the value CMS pays on, including the 1.0 floor where legislated for the year.',
|
|
1663
|
+
source: GPCI_SOURCE,
|
|
1664
|
+
data_as_of: `CY ${year}`,
|
|
1665
|
+
loaded_years: years,
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1144
1669
|
async function callTool(name: string, args: Record<string, unknown>): Promise<unknown> {
|
|
1145
1670
|
// FQHC PPS is a hand-maintained constant table (see FQHC_PPS_BASE_RATES
|
|
1146
1671
|
// above) — not Supabase-backed like the rest of this pack — so it is
|
|
1147
1672
|
// dispatched before the credential check below, which every OTHER tool
|
|
1148
1673
|
// here needs.
|
|
1149
1674
|
if (name === 'medicare_fqhc_pps_rate') return fqhcPpsRate(args);
|
|
1675
|
+
if (name === 'medicare_fqhc_gaf') return fqhcGafLookup(args);
|
|
1150
1676
|
|
|
1151
1677
|
const url = (args._supabaseUrl as string | undefined)?.trim();
|
|
1152
1678
|
const key = (args._supabaseKey as string | undefined)?.trim();
|
|
@@ -1156,6 +1682,9 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1156
1682
|
const cfg: Cfg = { url, key };
|
|
1157
1683
|
|
|
1158
1684
|
switch (name) {
|
|
1685
|
+
case 'medicare_gpci_lookup':
|
|
1686
|
+
return gpciLookup(cfg, args);
|
|
1687
|
+
|
|
1159
1688
|
case 'medicare_physician_payment': {
|
|
1160
1689
|
const hcpcs = code(args.code);
|
|
1161
1690
|
if (!hcpcs) return { found: false, reason: 'no_code', hint: 'Pass code, e.g. "99213".' };
|
|
@@ -1407,6 +1936,65 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1407
1936
|
};
|
|
1408
1937
|
}
|
|
1409
1938
|
|
|
1939
|
+
case 'medicare_opps_rate': {
|
|
1940
|
+
const hcpcs = code(args.code);
|
|
1941
|
+
if (!hcpcs) return { found: false, reason: 'no_code', hint: 'Pass code, e.g. "33289" or "C2624".' };
|
|
1942
|
+
const askedYear = typeof args.year === 'number' ? Math.floor(args.year) : null;
|
|
1943
|
+
const askedQuarterRaw = typeof args.quarter === 'string' ? args.quarter : null;
|
|
1944
|
+
const askedQuarter = resolveQuarter(askedQuarterRaw);
|
|
1945
|
+
if (askedQuarterRaw && !askedQuarter) {
|
|
1946
|
+
return { found: false, reason: 'bad_quarter', hint: `"${askedQuarterRaw}" is not a recognized quarter. Pass "Q1".."Q4" or the release month ("january"/"april"/"july"/"october").` };
|
|
1947
|
+
}
|
|
1948
|
+
const rows = await pg<OppsRow>(cfg,
|
|
1949
|
+
`opps_rates?hcpcs=eq.${hcpcs}${askedYear ? `&year=eq.${askedYear}` : ''}${askedQuarter ? `&quarter=eq.${askedQuarter}` : ''}`
|
|
1950
|
+
+ '&select=*&order=year.desc,quarter.desc&limit=5');
|
|
1951
|
+
if (!rows.length) {
|
|
1952
|
+
const any = await pg<{ year: number; quarter: string }>(cfg, 'opps_rates?select=year,quarter&order=year.desc,quarter.desc&limit=1');
|
|
1953
|
+
const vintageMismatch = any.length && ((askedYear && any[0].year !== askedYear) || (askedQuarter && any[0].quarter !== askedQuarter));
|
|
1954
|
+
return {
|
|
1955
|
+
found: false,
|
|
1956
|
+
reason: vintageMismatch ? 'year_not_loaded' : 'code_not_found',
|
|
1957
|
+
hint: vintageMismatch
|
|
1958
|
+
? `No ${askedYear ?? '(any year)'} ${askedQuarter ?? '(any quarter)'} OPPS release is loaded; the newest is ${any[0].year} ${any[0].quarter}. Rates differ every quarter, so this will not answer with a different vintage's numbers.`
|
|
1959
|
+
: `${hcpcs} is not on the OPPS Addendum B HCPCS list for the loaded vintage. This may be a physician, lab, or DMEPOS code instead — try medicare_physician_payment, medicare_lab_rate, or medicare_dmepos_rate.`,
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
const r = rows[0];
|
|
1963
|
+
const si = r.status_indicator;
|
|
1964
|
+
return {
|
|
1965
|
+
found: true,
|
|
1966
|
+
code: hcpcs,
|
|
1967
|
+
// Vintage first, deliberately: it qualifies every number below it.
|
|
1968
|
+
fee_schedule_year: r.year,
|
|
1969
|
+
quarter: r.quarter,
|
|
1970
|
+
year_was_defaulted: askedYear === null,
|
|
1971
|
+
quarter_was_defaulted: askedQuarter === null,
|
|
1972
|
+
status_indicator: si,
|
|
1973
|
+
status_indicator_meaning: OPPS_SI[si] ?? `Status indicator ${si} (not in the mapped set)`,
|
|
1974
|
+
apc: r.apc,
|
|
1975
|
+
apc_note: r.apc == null
|
|
1976
|
+
? 'No APC is assigned for this status indicator — the code is not separately paid under OPPS (often packaged into another service\'s payment, or not an outpatient-department benefit at all). See status_indicator_meaning.'
|
|
1977
|
+
: null,
|
|
1978
|
+
relative_weight: r.relative_weight,
|
|
1979
|
+
payment_rate_usd: r.payment_rate_usd,
|
|
1980
|
+
national_unadjusted_copay_usd: r.national_unadjusted_copay_usd,
|
|
1981
|
+
minimum_unadjusted_copay_usd: r.minimum_unadjusted_copay_usd,
|
|
1982
|
+
ira_coinsurance_pct: r.ira_coinsurance_pct,
|
|
1983
|
+
adjusted_beneficiary_copay_usd: r.adjusted_beneficiary_copay_usd,
|
|
1984
|
+
device_pass_through: r.device_pass_through,
|
|
1985
|
+
drug_or_biological_pass_through: r.drug_or_biological_pass_through,
|
|
1986
|
+
pass_through_note: r.device_pass_through
|
|
1987
|
+
? 'This device is CURRENTLY on CMS pass-through status (SI=H): paid separately, outside the standard APC amount, for as long as that status lasts — typically 2-3 years from market entry, after which it is repriced (often packaged, SI=N).'
|
|
1988
|
+
: r.drug_or_biological_pass_through
|
|
1989
|
+
? 'This drug/biological is CURRENTLY on CMS pass-through status (SI=G): paid separately at a rate outside the standard APC amount.'
|
|
1990
|
+
: null,
|
|
1991
|
+
changed_this_release: r.changed_this_release,
|
|
1992
|
+
note: r.note,
|
|
1993
|
+
source: 'CMS, OPPS Addendum B',
|
|
1994
|
+
caveat: 'National OPPS amount before sequestration, beneficiary coinsurance adjustments, and any wage-index/rural adjustment applied at the claim level. This is the hospital-outpatient-department rate; the same code billed in a physician office or ASC uses a different schedule.',
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1410
1998
|
case 'medicare_code_units_limit': {
|
|
1411
1999
|
const hcpcs = code(args.code);
|
|
1412
2000
|
if (!hcpcs) return { found: false, reason: 'no_code', hint: 'Pass code, e.g. "99213".' };
|
|
@@ -1522,13 +2110,14 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1522
2110
|
}
|
|
1523
2111
|
|
|
1524
2112
|
case 'medicare_pricing_coverage': {
|
|
1525
|
-
const [rvuN, gpciN, clfsN, mueN, ptpRange, dmeN] = await Promise.all([
|
|
2113
|
+
const [rvuN, gpciN, clfsN, mueN, ptpRange, dmeN, oppsN] = await Promise.all([
|
|
1526
2114
|
pg<{ year: number; release: string }>(cfg, 'pfs_rvu?select=year,release&order=year.desc&limit=1'),
|
|
1527
2115
|
pg<{ year: number }>(cfg, 'pfs_gpci?select=year&order=year.desc&limit=1'),
|
|
1528
2116
|
pg<{ year: number; quarter: string }>(cfg, 'clfs_rates?select=year,quarter&order=year.desc,quarter.desc&limit=1'),
|
|
1529
2117
|
pg<{ effective_date: string }>(cfg, 'ncci_mue?select=effective_date&order=effective_date.desc&limit=1'),
|
|
1530
2118
|
pg<{ first_seen_quarter: string; last_seen_quarter: string }>(cfg, 'ncci_ptp_edits?select=first_seen_quarter,last_seen_quarter&order=last_seen_quarter.desc&limit=1'),
|
|
1531
2119
|
pg<{ year: number; release: string }>(cfg, 'dmepos_rates?select=year,release&order=year.desc,release.desc&limit=1'),
|
|
2120
|
+
pg<{ year: number; quarter: string }>(cfg, 'opps_rates?select=year,quarter&order=year.desc,quarter.desc&limit=1'),
|
|
1532
2121
|
]);
|
|
1533
2122
|
return {
|
|
1534
2123
|
found: true,
|
|
@@ -1536,6 +2125,7 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1536
2125
|
geographic_localities: gpciN.length ? { year: gpciN[0].year } : null,
|
|
1537
2126
|
clinical_lab_fee_schedule: clfsN.length ? { year: clfsN[0].year, quarter: clfsN[0].quarter } : null,
|
|
1538
2127
|
dmepos_fee_schedule: dmeN.length ? { year: dmeN[0].year, release: dmeN[0].release, release_month: DME_RELEASE_MONTH[dmeN[0].release] ?? null, priced_by: 'state (not Medicare locality), with separate rural/non-rural amounts' } : null,
|
|
2128
|
+
hospital_outpatient_pps: oppsN.length ? { year: oppsN[0].year, quarter: oppsN[0].quarter, priced_by: 'HCPCS code, hospital outpatient department setting (status indicator + APC), source: CMS OPPS Addendum B' } : null,
|
|
1539
2129
|
fqhc_pps_base_rate: { newest_loaded_year: FQHC_PPS_BASE_RATES[FQHC_PPS_BASE_RATES.length - 1].year, loaded_years: FQHC_PPS_BASE_RATES.map((r) => r.year), source: FQHC_SOURCE },
|
|
1540
2130
|
medically_unlikely_edits: mueN.length ? { effective_from: mueN[0].effective_date } : null,
|
|
1541
2131
|
procedure_to_procedure_edits: ptpRange.length ? {
|
|
@@ -1543,10 +2133,10 @@ async function callTool(name: string, args: Record<string, unknown>): Promise<un
|
|
|
1543
2133
|
quarterly_overlay_window: '2025q1..2026q3 (re-applied on top of the baseline so a pair the quarterly files show deleted cannot be resurrected by an older baseline row)',
|
|
1544
2134
|
coverage: 'FULL, not a rolling window: a pair with no row here has never had a CMS-recorded PTP edit as of the loaded vintage. Before fleet #1293 (2026-09-07) this only covered the 2025q1..2026q3 change window and a NOT-FOUND answer could not rule out a pre-2025 edit.',
|
|
1545
2135
|
} : null,
|
|
1546
|
-
refresh_cadence: 'Physician fee schedule annually (with intra-year releases); lab fee schedule, DMEPOS fee schedule, MUE units edits, and PTP pair-edit changes all quarterly (as necessary).',
|
|
2136
|
+
refresh_cadence: 'Physician fee schedule annually (with intra-year releases); lab fee schedule, DMEPOS fee schedule, OPPS hospital-outpatient fee schedule, MUE units edits, and PTP pair-edit changes all quarterly (as necessary).',
|
|
1547
2137
|
no_descriptions_note:
|
|
1548
|
-
'CPT procedure descriptions (physician fee schedule, lab schedule) are not returned — American Medical Association copyright. DMEPOS is the exception: it
|
|
1549
|
-
source: 'CMS Physician Fee Schedule, Clinical Laboratory Fee Schedule, DMEPOS fee schedule, FQHC PPS base payment rate, and National Correct Coding Initiative (MUE + PTP quarterly change files)',
|
|
2138
|
+
'CPT procedure descriptions (physician fee schedule, lab schedule, OPPS) are not returned — American Medical Association copyright, and OPPS Addendum B mixes AMA CPT codes with CMS-owned HCPCS Level II codes under one descriptor column with no per-row way to separate them. DMEPOS is the exception: it is HCPCS Level II only, which CMS itself maintains, so medicare_dmepos_rate DOES return the code description.',
|
|
2139
|
+
source: 'CMS Physician Fee Schedule, Clinical Laboratory Fee Schedule, DMEPOS fee schedule, OPPS Addendum B, FQHC PPS base payment rate, and National Correct Coding Initiative (MUE + PTP quarterly change files)',
|
|
1550
2140
|
};
|
|
1551
2141
|
}
|
|
1552
2142
|
|
package/src/server.ts
CHANGED