Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a1fc137
Add Wave A production-readiness design spec
AndresL230 May 13, 2026
a0afa37
Add Wave A implementation plan
AndresL230 May 13, 2026
bbda447
ci: add ruff + mypy + pytest matrix on Python 3.9-3.12
AndresL230 May 13, 2026
2f6b8e0
fix(types): annotate to_dict returns as dict[str, Any] for mypy strict
AndresL230 May 13, 2026
c37e6c3
fix(interceptor): mypy strict — type wrappers, latency_ms float, drop…
AndresL230 May 13, 2026
d556c8b
fix: clean remaining mypy/ruff issues across recost/
AndresL230 May 13, 2026
170b55a
fix(interceptor): use bare 'raise' to preserve traceback context (#13)
AndresL230 May 13, 2026
9263bbf
fix: remove unused exception variables in _interceptor.py
AndresL230 May 13, 2026
67a7e74
fix: post-rebase cleanup of unused imports and stale type-ignore
AndresL230 May 13, 2026
e298ae6
feat(errors): add RecostError base class
AndresL230 May 13, 2026
3c483ca
feat(init): record current pid on RecostHandle for fork-safety backstop
AndresL230 May 13, 2026
2d610bd
feat(init): add RecostHandle.reinit_after_fork for explicit fork reco…
AndresL230 May 13, 2026
0adf7f4
feat(init): register os.register_at_fork hook for automatic child rei…
AndresL230 May 13, 2026
757e321
feat(init): PID backstop on event path for hook-less fork environment…
AndresL230 May 13, 2026
513d390
fix(interceptor): guard reentrancy across tasks AND threads (#15)
AndresL230 May 13, 2026
7864067
fix(interceptor): revert overbuilt Thread.__init__ patch; keep dual g…
AndresL230 May 13, 2026
79350a7
feat(errors): add RecostAuthError and RecostFatalAuthError
AndresL230 May 13, 2026
88bb42d
feat(errors): add RecostRateLimitError
AndresL230 May 13, 2026
0be6fd6
feat(init): validate api_key format at init() (#17)
AndresL230 May 13, 2026
2775ec9
refactor(transport): _post_cloud returns _CloudResult with parsed Ret…
AndresL230 May 13, 2026
45ca77f
feat(transport): 401 escalation — RecostAuthError per failure, Fatal …
AndresL230 May 13, 2026
8949b80
feat(transport): honor 429 Retry-After — emit RateLimitError, defer n…
AndresL230 May 13, 2026
ff5fd34
fix(interceptor): measure aiohttp json= and FormData body sizes (#8)
AndresL230 May 13, 2026
02ca557
fix(interceptor): guard httpx streaming bodies — never materialize it…
AndresL230 May 13, 2026
4d20a75
docs(readme): clarify streaming caveats for request/response byte counts
AndresL230 May 13, 2026
c4f9ba1
fix(tests): use rc- prefix in atexit subprocess test for api_key vali…
AndresL230 May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[dev,all]"
- name: Ruff
run: ruff check recost/ tests/
- name: Mypy
run: mypy recost/
- name: Pytest
run: pytest -v
3 changes: 2 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,8 @@ Unrecognized hosts still appear in telemetry, grouped under `"unknown"`.
- Request timestamp, method, URL (query params stripped), host, path
- Response status code
- Round-trip latency (ms)
- Request and response body size (bytes)
- Request body size (bytes) — measured for JSON, form, bytes, and string payloads. Streaming uploads (async iterators, generators) are reported as 0 to avoid buffering large bodies.
- Response body size (bytes) — derived from the `Content-Length` response header. HTTP chunked and SSE streams do not set this header and will report 0.
- Matched provider, endpoint category, and estimated cost

**Never captured:**
Expand Down
Loading
Loading