diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e90b515389..95fe90cc45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2196,6 +2196,37 @@ importers: specifier: 'catalog:' version: 4.98.0(@cloudflare/workers-types@4.20260605.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + services/headroom-compress: + dependencies: + '@cloudflare/containers': + specifier: 0.3.7 + version: 0.3.7 + '@kilocode/worker-utils': + specifier: workspace:* + version: link:../../packages/worker-utils + hono: + specifier: 4.12.18 + version: 4.12.18 + devDependencies: + '@cloudflare/workers-types': + specifier: 'catalog:' + version: 4.20260605.1 + '@types/node': + specifier: 'catalog:' + version: 24.12.4 + '@typescript/native-preview': + specifier: 'catalog:' + version: 7.0.0-dev.20260514.1 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vitest: + specifier: 'catalog:' + version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + wrangler: + specifier: 'catalog:' + version: 4.98.0(@cloudflare/workers-types@4.20260605.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + services/images-mcp: dependencies: '@modelcontextprotocol/sdk': @@ -17930,9 +17961,9 @@ snapshots: '@chromaui/rrweb-snapshot': 2.0.0-alpha.18-noAbsolute '@playwright/test': 1.58.2 '@segment/analytics-node': 2.1.3 - '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/csf': 0.1.13 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/server-webpack5': 8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -17958,9 +17989,9 @@ snapshots: '@chromaui/rrweb-snapshot': 2.0.0-alpha.18-noAbsolute '@playwright/test': 1.58.2 '@segment/analytics-node': 2.1.3 - '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-essentials': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/csf': 0.1.13 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/server-webpack5': 8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -18053,7 +18084,7 @@ snapshots: cjs-module-lexer: 1.2.3 esbuild: 0.27.4 miniflare: 4.20260603.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) wrangler: 4.98.0(@cloudflare/workers-types@4.20260605.1)(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 transitivePeerDependencies: @@ -23036,7 +23067,7 @@ snapshots: '@stitches/core@1.2.8': {} - '@storybook/addon-actions@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-actions@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 @@ -23045,26 +23076,26 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) uuid: 9.0.1 - '@storybook/addon-backgrounds@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-backgrounds@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 memoizerific: 1.11.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-controls@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-controls@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-docs@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.6) - '@storybook/blocks': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/csf-plugin': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/react-dom-shim': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/blocks': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/csf-plugin': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/react-dom-shim': 8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) react: 19.2.6 react-dom: 19.2.4(react@19.2.6) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23085,23 +23116,23 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@storybook/addon-essentials@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': - dependencies: - '@storybook/addon-actions': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-backgrounds': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-controls': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-docs': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-highlight': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-measure': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-outline': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-toolbars': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/addon-viewport': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/addon-essentials@8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + '@storybook/addon-actions': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-backgrounds': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-controls': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-docs': 8.5.8(@types/react@19.2.14)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-highlight': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-measure': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-outline': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-toolbars': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/addon-viewport': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-highlight@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23113,13 +23144,13 @@ snapshots: optionalDependencies: react: 19.2.6 - '@storybook/addon-measure@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-measure@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) tiny-invariant: 1.3.3 - '@storybook/addon-outline@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-outline@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/global': 5.0.0 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23130,16 +23161,16 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/addon-toolbars@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-toolbars@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/addon-viewport@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/addon-viewport@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: memoizerific: 1.11.3 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/blocks@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/blocks@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: '@storybook/csf': 0.1.12 '@storybook/icons': 1.6.0(react-dom@19.2.4(react@19.2.6))(react@19.2.6) @@ -23151,7 +23182,7 @@ snapshots: '@storybook/builder-webpack5@8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@types/semver': 7.7.1 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 @@ -23187,7 +23218,7 @@ snapshots: '@storybook/builder-webpack5@8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3)': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@types/semver': 7.7.1 browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 @@ -23249,11 +23280,11 @@ snapshots: - uglify-js - webpack-cli - '@storybook/components@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/components@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@storybook/core-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/core-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 @@ -23263,7 +23294,7 @@ snapshots: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 - '@storybook/csf-plugin@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/csf-plugin@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) unplugin: 1.16.1 @@ -23288,7 +23319,7 @@ snapshots: react: 19.2.6 react-dom: 19.2.4(react@19.2.6) - '@storybook/manager-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/manager-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23376,17 +23407,17 @@ snapshots: - uglify-js - webpack-cli - '@storybook/preset-server-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/preset-server-webpack@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: - '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/core-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/global': 5.0.0 - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) safe-identifier: 0.4.2 storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 yaml-loader: 0.8.1 - '@storybook/preview-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/preview-api@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -23404,7 +23435,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/react-dom-shim@8.5.8(react-dom@19.2.4(react@19.2.6))(react@19.2.6)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: react: 19.2.6 react-dom: 19.2.4(react@19.2.6) @@ -23435,8 +23466,8 @@ snapshots: '@storybook/server-webpack5@8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)': dependencies: '@storybook/builder-webpack5': 8.5.8(@swc/core@1.15.18)(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3) - '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) transitivePeerDependencies: - '@rspack/core' @@ -23449,8 +23480,8 @@ snapshots: '@storybook/server-webpack5@8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3)': dependencies: '@storybook/builder-webpack5': 8.5.8(esbuild@0.27.4)(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))(typescript@5.9.3) - '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/preset-server-webpack': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/server': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) transitivePeerDependencies: - '@rspack/core' @@ -23461,14 +23492,14 @@ snapshots: - webpack-cli optional: true - '@storybook/server@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/server@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: - '@storybook/components': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/components': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@storybook/csf': 0.1.12 '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/preview-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) - '@storybook/theming': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))) + '@storybook/manager-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/preview-api': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + '@storybook/theming': 8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)) storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) ts-dedent: 2.2.0 yaml: 2.8.4 @@ -23503,7 +23534,7 @@ snapshots: - supports-color - ts-node - '@storybook/theming@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)))': + '@storybook/theming@8.5.8(storybook@9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6))': dependencies: storybook: 9.1.20(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vite@8.0.10(@types/node@25.5.2)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) @@ -24165,7 +24196,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/expect@3.2.4': dependencies: @@ -24251,7 +24282,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.5.2)(@vitest/coverage-v8@4.1.6)(@vitest/ui@4.1.6)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/utils@3.2.4': dependencies: diff --git a/services/headroom-compress/.dev.vars.example b/services/headroom-compress/.dev.vars.example new file mode 100644 index 0000000000..688131b224 --- /dev/null +++ b/services/headroom-compress/.dev.vars.example @@ -0,0 +1 @@ +HEADROOM_BEARER_TOKEN=dev-headroom-token diff --git a/services/headroom-compress/README.md b/services/headroom-compress/README.md new file mode 100644 index 0000000000..d9a51a7f42 --- /dev/null +++ b/services/headroom-compress/README.md @@ -0,0 +1,51 @@ +# Headroom Compress Worker + +Worker-gated Cloudflare Container deployment for Headroom compression only. + +Public surface: + +- `GET /readyz` +- `POST /v1/compress` + +All other Headroom routes return `404` at Worker layer before container fetch. + +Required secret: + +```bash +pnpm exec wrangler secrets-store secret create HEADROOM_BEARER_TOKEN +``` + +Build and push pinned source image from a native amd64 builder: + +```bash +pnpm run container:build +``` + +Current deployed fallback tag is `0.27.0-ghcr9f5f0de`, mirrored from the +published `v0.27.0` amd64 image digest because local arm64 Docker cannot build +Headroom's amd64 Rust extension under QEMU. + +Deploy: + +```bash +pnpm run deploy +``` + +Smoke test: + +```bash +curl --fail https://headroom.kiloapps.io/readyz + +curl --fail https://headroom.kiloapps.io/v1/compress \ + -H "authorization: Bearer $HEADROOM_BEARER_TOKEN" \ + -H "content-type: application/json" \ + --data '{"model":"kilo/anthropic/claude-sonnet-4.6","messages":[{"role":"user","content":"hello"}],"config":{"compress_user_messages":true}}' +``` + +Benchmark compression: + +```bash +pnpm run benchmark:compression -- --case logs --repeat 3 +pnpm run benchmark:compression -- --list-cases +pnpm run benchmark:compression -- --fixture ./messages.json --json --output report.json +``` diff --git a/services/headroom-compress/container-build-context/Dockerfile b/services/headroom-compress/container-build-context/Dockerfile new file mode 100644 index 0000000000..7c99b25157 --- /dev/null +++ b/services/headroom-compress/container-build-context/Dockerfile @@ -0,0 +1,98 @@ +# syntax=docker/dockerfile:1.7 + +ARG HEADROOM_REF=da1a3973ed79d89617087ec315e77fb82356c03b +ARG TARGETPLATFORM=linux/amd64 +ARG PYTHON_VERSION=3.13 +ARG PYTHON_SITE_PACKAGES=/usr/local/lib/python${PYTHON_VERSION}/site-packages + +FROM --platform=${TARGETPLATFORM} alpine:3.22 AS source +ARG HEADROOM_REF +RUN apk add --no-cache ca-certificates curl tar +WORKDIR /src +RUN curl -fsSL "https://github.com/headroomlabs-ai/headroom/archive/${HEADROOM_REF}.tar.gz" \ + | tar -xz --strip-components=1 + +FROM --platform=${TARGETPLATFORM} python:${PYTHON_VERSION}-slim AS builder + +ARG PYTHON_SITE_PACKAGES + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + curl \ + g++ \ + patchelf \ + && rm -rf /var/lib/apt/lists/* + +ENV CARGO_HOME=/usr/local/cargo \ + RUSTUP_HOME=/usr/local/rustup \ + PATH=/usr/local/cargo/bin:${PATH} +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal -c rustfmt -c clippy --default-toolchain 1.95.0 + +WORKDIR /build +COPY --from=source /src/pyproject.toml /src/uv.lock /src/README.md ./ +COPY --from=source /src/Cargo.toml /src/Cargo.lock /src/rust-toolchain.toml ./ +COPY --from=source /src/crates/ crates/ +COPY --from=source /src/headroom/ headroom/ + +ARG HEADROOM_EXTRAS=proxy,code +RUN --mount=type=cache,target=/root/.cache/pip \ + --mount=type=cache,target=/root/.cargo/registry \ + --mount=type=cache,target=/build/target \ + python -m pip install ".[${HEADROOM_EXTRAS}]" + +RUN cd /tmp && python -c "from headroom._core import DiffCompressor, SmartCrusher; print(f'headroom core OK: {DiffCompressor.__name__}, {SmartCrusher.__name__}')" + +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/build/target \ + cargo build --release --locked --bin headroom-proxy && \ + cp target/release/headroom-proxy /usr/local/bin/headroom-proxy + +FROM --platform=${TARGETPLATFORM} python:${PYTHON_VERSION}-slim AS runtime + +ARG RUNTIME_USER=nonroot +ARG RUNTIME_HOME=/home/nonroot +ARG PYTHON_SITE_PACKAGES + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=builder ${PYTHON_SITE_PACKAGES} ${PYTHON_SITE_PACKAGES} +COPY --from=builder /usr/local/bin/headroom /usr/local/bin/headroom +COPY --from=builder /usr/local/bin/headroom-proxy /usr/local/bin/headroom-proxy + +RUN mkdir -p ${RUNTIME_HOME}/.cache/huggingface ${RUNTIME_HOME}/.headroom /data && \ + if [ "$RUNTIME_USER" = "nonroot" ]; then \ + groupadd --gid 1000 nonroot && \ + useradd --uid 1000 --gid nonroot --create-home nonroot && \ + chown -R nonroot:nonroot /data ${RUNTIME_HOME}; \ + fi + +USER ${RUNTIME_USER} +WORKDIR ${RUNTIME_HOME} + +ENV HEADROOM_HOST=0.0.0.0 \ + HEADROOM_PORT=8787 \ + HEADROOM_IN_DOCKER=1 \ + HF_HOME=${RUNTIME_HOME}/.cache/huggingface \ + TRANSFORMERS_CACHE=${RUNTIME_HOME}/.cache/huggingface \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + OMP_NUM_THREADS=1 + +RUN HEADROOM_STATELESS=true \ + HEADROOM_TELEMETRY=off \ + HEADROOM_SKIP_UPSTREAM_CHECK=1 \ + HEADROOM_MODEL_LIMITS='{"context_limits":{"kilo/anthropic/claude-sonnet-4.6":1000000}}' \ + python -c "from headroom import compress; text=('alpha beta gamma delta epsilon zeta eta theta\\n' * 256); result=compress([{'role':'user','content':text}], model='kilo/anthropic/claude-sonnet-4.6', model_limit=1000000, compress_user_messages=True, protect_recent=0, min_tokens_to_compress=10); print(f'headroom warmup OK: {result.tokens_before}->{result.tokens_after}')" + +EXPOSE 8787 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD ["curl", "--fail", "--silent", "http://127.0.0.1:8787/readyz"] + +ENTRYPOINT ["headroom", "proxy"] +CMD ["--host", "0.0.0.0", "--port", "8787"] diff --git a/services/headroom-compress/container/HEADROOM_PIN.md b/services/headroom-compress/container/HEADROOM_PIN.md new file mode 100644 index 0000000000..06802eeb41 --- /dev/null +++ b/services/headroom-compress/container/HEADROOM_PIN.md @@ -0,0 +1,15 @@ +# Headroom Pin + +- Upstream repo: `https://github.com/headroomlabs-ai/headroom` +- Commit: `da1a3973ed79d89617087ec315e77fb82356c03b` +- Version: `0.27.0` +- Preferred source-build image tag: `headroom-compress:0.27.0-da1a397` +- Deployed fallback image tag: `headroom-compress:0.27.0-ghcr9f5f0de` +- Deployed fallback source: `ghcr.io/chopratejas/headroom@sha256:9f5f0de34dbb4c2ba2b60ebba9bb2c28c9a07664629f3c1c0e9ea86cead62631` +- Cloudflare Registry image: `registry.cloudflare.com/e115e769bcdd4c3d66af59d3332cb394/headroom-compress:0.27.0-ghcr9f5f0de` +- Platform: `linux/amd64` + +Build from `../container-build-context/Dockerfile`. Do not deploy `latest`. + +On arm64 Docker Desktop, the pinned source build currently fails because amd64 +Rust tooling segfaults under QEMU. Use a native amd64 builder for source builds. diff --git a/services/headroom-compress/container/README.md b/services/headroom-compress/container/README.md new file mode 100644 index 0000000000..1dda174252 --- /dev/null +++ b/services/headroom-compress/container/README.md @@ -0,0 +1,23 @@ +# Headroom Container + +Build pinned Cloudflare Container image from source. This requires a native amd64 +builder because Headroom builds Rust extensions: + +```bash +cd services/headroom-compress +pnpm run container:build +``` + +Confirm pushed image: + +```bash +pnpm exec wrangler containers images list --filter headroom-compress --json +``` + +Deploy only the pinned tag referenced in `wrangler.jsonc`. Record returned digest in release notes before deploy. + +Current fallback image was mirrored from: + +```text +ghcr.io/chopratejas/headroom@sha256:9f5f0de34dbb4c2ba2b60ebba9bb2c28c9a07664629f3c1c0e9ea86cead62631 +``` diff --git a/services/headroom-compress/package.json b/services/headroom-compress/package.json new file mode 100644 index 0000000000..12d5a98c67 --- /dev/null +++ b/services/headroom-compress/package.json @@ -0,0 +1,30 @@ +{ + "name": "headroom-compress", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "tail": "wrangler tail", + "types": "wrangler types --include-runtime=false --env-file /dev/null", + "typecheck": "tsgo --noEmit", + "lint": "pnpm -w exec oxlint --config .oxlintrc.json services/headroom-compress/src", + "test": "vitest run", + "container:build": "scripts/build-headroom-image.sh", + "benchmark:compression": "node scripts/benchmark-compression.mjs" + }, + "dependencies": { + "@cloudflare/containers": "0.3.7", + "@kilocode/worker-utils": "workspace:*", + "hono": "catalog:" + }, + "devDependencies": { + "@cloudflare/workers-types": "catalog:", + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:", + "wrangler": "catalog:" + } +} diff --git a/services/headroom-compress/scripts/benchmark-compression.mjs b/services/headroom-compress/scripts/benchmark-compression.mjs new file mode 100755 index 0000000000..2745fa4d06 --- /dev/null +++ b/services/headroom-compress/scripts/benchmark-compression.mjs @@ -0,0 +1,485 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { performance } from 'node:perf_hooks'; +import { fileURLToPath } from 'node:url'; + +const DEFAULT_URL = 'https://headroom.kiloapps.io'; +const DEFAULT_MODEL = 'kilo/anthropic/claude-sonnet-4.6'; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const serviceDir = resolve(scriptDir, '..'); + +const builtInCases = { + logs: { + description: 'Repeated application logs with error traces and JSON context.', + messages: [ + { + role: 'user', + content: buildLogPayload(), + }, + ], + config: { compress_user_messages: true, protect_recent: 0, target_ratio: 0.35 }, + }, + json: { + description: 'Large repetitive JSON API payload.', + messages: [ + { + role: 'user', + content: JSON.stringify(buildJsonPayload(), null, 2), + }, + ], + config: { compress_user_messages: true, protect_recent: 0, target_ratio: 0.35 }, + }, + prose: { + description: 'Long redundant prose notes.', + messages: [ + { + role: 'user', + content: buildProsePayload(), + }, + ], + config: { compress_user_messages: true, protect_recent: 0, target_ratio: 0.4 }, + }, +}; + +async function main() { + loadLocalDevVars(); + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printUsage(); + return; + } + if (args.listCases) { + printCases(); + return; + } + + const benchmarkCase = loadBenchmarkCase(args); + const url = trimTrailingSlash(args.url ?? process.env.HEADROOM_COMPRESS_URL ?? DEFAULT_URL); + const token = args.token ?? process.env.HEADROOM_BEARER_TOKEN; + if (!token) { + throw new Error('Missing bearer token. Set HEADROOM_BEARER_TOKEN or create .dev.vars.'); + } + + const model = args.model ?? benchmarkCase.model ?? DEFAULT_MODEL; + const repeat = args.repeat ?? 1; + const minSavedTokens = args.minSavedTokens ?? 1; + const minSavingsRatio = args.minSavingsRatio ?? 0.01; + const requestBody = { + model, + messages: benchmarkCase.messages, + config: benchmarkCase.config ?? { compress_user_messages: true, protect_recent: 0 }, + ...(benchmarkCase.token_budget ? { token_budget: benchmarkCase.token_budget } : {}), + }; + + const results = []; + for (let index = 0; index < repeat; index += 1) { + results.push(await runOnce({ url, token, body: requestBody, index })); + } + + const summary = summarizeResults(results, { + url, + model, + caseName: benchmarkCase.name, + minSavedTokens, + minSavingsRatio, + }); + + if (args.json) { + console.log(JSON.stringify(summary, null, 2)); + } else { + printSummary(summary); + } + + if (args.output) { + writeFileSync(resolve(args.output), JSON.stringify(summary, null, 2) + '\n'); + } + + if (!summary.passed) { + process.exitCode = 1; + } +} + +function loadBenchmarkCase(args) { + if (args.fixture) { + const fixturePath = resolve(args.fixture); + const parsed = JSON.parse(readFileSync(fixturePath, 'utf8')); + const fixture = Array.isArray(parsed) ? { messages: parsed } : parsed; + if (!Array.isArray(fixture.messages)) { + throw new Error('Fixture must be a messages array or an object with messages array.'); + } + return { + name: args.caseName ?? fixturePath, + description: 'Custom fixture', + ...fixture, + }; + } + + const caseName = args.caseName ?? 'logs'; + const selected = builtInCases[caseName]; + if (!selected) { + throw new Error(`Unknown case "${caseName}". Use --list-cases.`); + } + return { name: caseName, ...selected }; +} + +async function runOnce({ url, token, body, index }) { + const startedAt = performance.now(); + const response = await fetch(`${url}/v1/compress`, { + method: 'POST', + headers: { + authorization: `Bearer ${token}`, + 'content-type': 'application/json', + 'x-request-id': `headroom-benchmark-${Date.now()}-${index}`, + }, + body: JSON.stringify(body), + }); + const durationMs = performance.now() - startedAt; + const text = await response.text(); + const parsed = parseJsonBody(text); + + if (!response.ok) { + throw new Error(`Compression request failed: ${response.status} ${text.slice(0, 500)}`); + } + + const tokensBefore = numberField(parsed, 'tokens_before'); + const tokensAfter = numberField(parsed, 'tokens_after'); + const tokensSaved = numberField(parsed, 'tokens_saved'); + const savingsRatio = tokensBefore > 0 ? tokensSaved / tokensBefore : 0; + const originalBytes = Buffer.byteLength(JSON.stringify(body.messages)); + const compressedBytes = Buffer.byteLength(JSON.stringify(parsed.messages ?? [])); + + return { + index, + status: response.status, + durationMs: Math.round(durationMs), + requestId: response.headers.get('x-request-id'), + tokensBefore, + tokensAfter, + tokensSaved, + compressionRatio: numberField(parsed, 'compression_ratio'), + savingsRatio, + originalBytes, + compressedBytes, + byteSavingsRatio: originalBytes > 0 ? (originalBytes - compressedBytes) / originalBytes : 0, + transformsApplied: Array.isArray(parsed.transforms_applied) ? parsed.transforms_applied : [], + transformsSummary: parsed.transforms_summary ?? {}, + ccrHashCount: Array.isArray(parsed.ccr_hashes) ? parsed.ccr_hashes.length : 0, + }; +} + +function summarizeResults(results, config) { + const totals = results.reduce( + (acc, result) => { + acc.durationMs += result.durationMs; + acc.tokensBefore += result.tokensBefore; + acc.tokensAfter += result.tokensAfter; + acc.tokensSaved += result.tokensSaved; + acc.originalBytes += result.originalBytes; + acc.compressedBytes += result.compressedBytes; + return acc; + }, + { + durationMs: 0, + tokensBefore: 0, + tokensAfter: 0, + tokensSaved: 0, + originalBytes: 0, + compressedBytes: 0, + } + ); + const average = { + durationMs: Math.round(totals.durationMs / results.length), + tokensBefore: Math.round(totals.tokensBefore / results.length), + tokensAfter: Math.round(totals.tokensAfter / results.length), + tokensSaved: Math.round(totals.tokensSaved / results.length), + savingsRatio: totals.tokensBefore > 0 ? totals.tokensSaved / totals.tokensBefore : 0, + byteSavingsRatio: + totals.originalBytes > 0 + ? (totals.originalBytes - totals.compressedBytes) / totals.originalBytes + : 0, + }; + const passed = + results.every(result => result.tokensSaved >= config.minSavedTokens) && + results.every(result => result.savingsRatio >= config.minSavingsRatio); + + return { + passed, + url: config.url, + model: config.model, + caseName: config.caseName, + thresholds: { + minSavedTokens: config.minSavedTokens, + minSavingsRatio: config.minSavingsRatio, + }, + average, + totals, + results, + }; +} + +function printSummary(summary) { + console.log(`Headroom compression benchmark`); + console.log(`url: ${summary.url}`); + console.log(`model: ${summary.model}`); + console.log(`case: ${summary.caseName}`); + console.log(`passed: ${summary.passed ? 'yes' : 'no'}`); + console.log(''); + console.log( + [ + 'run', + 'status', + 'ms', + 'tokens_before', + 'tokens_after', + 'tokens_saved', + 'savings', + 'byte_savings', + 'transforms', + ].join('\t') + ); + for (const result of summary.results) { + console.log( + [ + result.index + 1, + result.status, + result.durationMs, + result.tokensBefore, + result.tokensAfter, + result.tokensSaved, + formatPercent(result.savingsRatio), + formatPercent(result.byteSavingsRatio), + result.transformsApplied.join(',') || 'none', + ].join('\t') + ); + } + console.log(''); + console.log( + `avg: ${summary.average.durationMs}ms, saved ${summary.average.tokensSaved}/${summary.average.tokensBefore} tokens (${formatPercent( + summary.average.savingsRatio + )})` + ); +} + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + switch (arg) { + case '--': + break; + case '--help': + case '-h': + args.help = true; + break; + case '--list-cases': + args.listCases = true; + break; + case '--url': + args.url = requireValue(argv, ++index, arg); + break; + case '--token': + args.token = requireValue(argv, ++index, arg); + break; + case '--model': + args.model = requireValue(argv, ++index, arg); + break; + case '--case': + args.caseName = requireValue(argv, ++index, arg); + break; + case '--fixture': + args.fixture = requireValue(argv, ++index, arg); + break; + case '--repeat': + args.repeat = positiveInt(requireValue(argv, ++index, arg), arg); + break; + case '--min-saved-tokens': + args.minSavedTokens = nonNegativeNumber(requireValue(argv, ++index, arg), arg); + break; + case '--min-savings-ratio': + args.minSavingsRatio = nonNegativeNumber(requireValue(argv, ++index, arg), arg); + break; + case '--json': + args.json = true; + break; + case '--output': + args.output = requireValue(argv, ++index, arg); + break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + return args; +} + +function printUsage() { + console.log(`Usage: pnpm run benchmark:compression -- [options] + +Options: + --url Worker URL. Default: ${DEFAULT_URL} + --model Model id. Default: ${DEFAULT_MODEL} + --case Built-in case. Default: logs + --fixture JSON messages array or { messages, config, token_budget } + --repeat Number of runs. Default: 1 + --min-saved-tokens Per-run minimum saved tokens. Default: 1 + --min-savings-ratio Per-run minimum saved/token_before ratio. Default: 0.01 + --json Print JSON report + --output Write JSON report + --list-cases List built-in cases +`); +} + +function printCases() { + for (const [name, value] of Object.entries(builtInCases)) { + console.log(`${name}\t${value.description}`); + } +} + +function loadLocalDevVars() { + if (process.env.HEADROOM_BEARER_TOKEN) return; + const devVarsPath = resolve(serviceDir, '.dev.vars'); + if (!existsSync(devVarsPath)) return; + + const lines = readFileSync(devVarsPath, 'utf8').split(/\r?\n/); + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const equalsIndex = trimmed.indexOf('='); + if (equalsIndex === -1) continue; + const key = trimmed.slice(0, equalsIndex).trim(); + const value = trimmed.slice(equalsIndex + 1).trim(); + if (key && process.env[key] === undefined) { + process.env[key] = unquote(value); + } + } +} + +function buildLogPayload() { + const lines = []; + for (let index = 0; index < 160; index += 1) { + const requestId = `req-${String(index % 12).padStart(3, '0')}`; + const tenant = `tenant-${index % 5}`; + const endpoint = ['/api/search', '/api/export', '/api/sync'][index % 3]; + const level = index % 13 === 0 ? 'ERROR' : index % 7 === 0 ? 'WARN' : 'INFO'; + lines.push( + [ + `2026-06-23T09:${String(index % 60).padStart(2, '0')}:00.000Z`, + level, + `request_id=${requestId}`, + `tenant=${tenant}`, + `endpoint=${endpoint}`, + `duration_ms=${180 + (index % 40)}`, + 'cache=miss', + 'region=iad', + 'message="retrieved 25 documents and normalized ranking features"', + ].join(' ') + ); + if (level === 'ERROR') { + lines.push( + `Traceback: Error: upstream timeout while fetching shard ${index % 4}`, + ' at fetchShard (/app/search.ts:42:11)', + ' at rankDocuments (/app/rank.ts:88:7)', + `context=${JSON.stringify({ requestId, tenant, endpoint, retryable: true })}` + ); + } + } + return lines.join('\n'); +} + +function buildJsonPayload() { + return { + query: 'benchmark compression regression fixture', + generated_at: '2026-06-23T09:00:00.000Z', + results: Array.from({ length: 120 }, (_, index) => ({ + id: `doc_${String(index).padStart(4, '0')}`, + source: ['docs', 'tickets', 'logs'][index % 3], + title: `Repeated benchmark result ${index % 10}`, + score: Number((0.91 - (index % 20) * 0.01).toFixed(3)), + tags: ['compression', 'benchmark', `bucket-${index % 8}`], + summary: + 'This record repeats enough structure and wording for Headroom to remove redundancy while preserving ranking signals.', + metadata: { + tenant: `tenant-${index % 5}`, + shard: index % 6, + permissions: ['read', 'export'], + pii: false, + }, + })), + }; +} + +function buildProsePayload() { + const paragraph = + 'The benchmark corpus describes the same incident repeatedly with minor wording changes. The important facts are request id, tenant, endpoint, latency, retry state, and final outcome. Compression should preserve those facts while removing redundant framing sentences.'; + return Array.from({ length: 90 }, (_, index) => { + return `Note ${index + 1}: ${paragraph} Tenant tenant-${index % 5} saw endpoint ${ + ['/api/search', '/api/export', '/api/sync'][index % 3] + } complete with retry state ${index % 4 === 0 ? 'retryable' : 'not_retryable'}.`; + }).join('\n\n'); +} + +function parseJsonBody(text) { + try { + return JSON.parse(text); + } catch { + throw new Error(`Expected JSON response, got: ${text.slice(0, 500)}`); + } +} + +function numberField(value, field) { + const fieldValue = value?.[field]; + if (typeof fieldValue !== 'number' || !Number.isFinite(fieldValue)) { + throw new Error(`Response missing numeric ${field}.`); + } + return fieldValue; +} + +function requireValue(argv, index, flag) { + const value = argv[index]; + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value.`); + } + return value; +} + +function positiveInt(value, flag) { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new Error(`${flag} must be a positive integer.`); + } + return parsed; +} + +function nonNegativeNumber(value, flag) { + const parsed = Number.parseFloat(value); + if (!Number.isFinite(parsed) || parsed < 0) { + throw new Error(`${flag} must be a non-negative number.`); + } + return parsed; +} + +function formatPercent(value) { + return `${(value * 100).toFixed(1)}%`; +} + +function trimTrailingSlash(value) { + return value.replace(/\/+$/, ''); +} + +function unquote(value) { + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1); + } + return value; +} + +main().catch(error => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); +}); diff --git a/services/headroom-compress/scripts/build-headroom-image.sh b/services/headroom-compress/scripts/build-headroom-image.sh new file mode 100755 index 0000000000..50faabce9c --- /dev/null +++ b/services/headroom-compress/scripts/build-headroom-image.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +tag="${HEADROOM_TAG:-0.27.0-da1a397}" + +pnpm exec wrangler containers build -p -t "headroom-compress:${tag}" ./container-build-context +pnpm exec wrangler containers images list --filter headroom-compress --json diff --git a/services/headroom-compress/src/auth.ts b/services/headroom-compress/src/auth.ts new file mode 100644 index 0000000000..6c36e60cbd --- /dev/null +++ b/services/headroom-compress/src/auth.ts @@ -0,0 +1,6 @@ +import { backendAuthMiddleware } from '@kilocode/worker-utils'; +import type { HonoEnv } from './hono-env'; + +export const authMiddleware = backendAuthMiddleware(async c => + c.env.HEADROOM_BEARER_TOKEN.get() +); diff --git a/services/headroom-compress/src/config.ts b/services/headroom-compress/src/config.ts new file mode 100644 index 0000000000..cbaa2870be --- /dev/null +++ b/services/headroom-compress/src/config.ts @@ -0,0 +1,92 @@ +export type ModelLimits = { + contextLimits: Record; +}; + +export type HeadroomRuntimeConfig = { + instanceCount: number; + modelAllowlist: Set; + modelLimits: ModelLimits; + maxBodyBytes: number; + maxMessages: number; + maxContentChars: number; + maxTokenBudget: number; + containerRequestTimeoutMs: number; +}; + +type ConfigEnv = Pick< + Env, + | 'HEADROOM_INSTANCE_COUNT' + | 'HEADROOM_MODEL_ALLOWLIST' + | 'HEADROOM_MODEL_LIMITS' + | 'HEADROOM_MAX_BODY_BYTES' + | 'HEADROOM_MAX_MESSAGES' + | 'HEADROOM_MAX_CONTENT_CHARS' + | 'HEADROOM_MAX_TOKEN_BUDGET' + | 'HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS' +>; + +export class ConfigError extends Error {} + +export function loadConfig(env: ConfigEnv): HeadroomRuntimeConfig { + const modelAllowlist = parseCsvSet(env.HEADROOM_MODEL_ALLOWLIST); + const modelLimits = parseModelLimits(env.HEADROOM_MODEL_LIMITS); + if (modelAllowlist.size === 0) { + throw new ConfigError('HEADROOM_MODEL_ALLOWLIST must contain at least one model'); + } + for (const model of modelAllowlist) { + if (!Number.isInteger(modelLimits.contextLimits[model])) { + throw new ConfigError(`HEADROOM_MODEL_LIMITS missing context limit for ${model}`); + } + } + + return { + instanceCount: parsePositiveInt(env.HEADROOM_INSTANCE_COUNT, 'HEADROOM_INSTANCE_COUNT'), + modelAllowlist, + modelLimits, + maxBodyBytes: parsePositiveInt(env.HEADROOM_MAX_BODY_BYTES, 'HEADROOM_MAX_BODY_BYTES'), + maxMessages: parsePositiveInt(env.HEADROOM_MAX_MESSAGES, 'HEADROOM_MAX_MESSAGES'), + maxContentChars: parsePositiveInt(env.HEADROOM_MAX_CONTENT_CHARS, 'HEADROOM_MAX_CONTENT_CHARS'), + maxTokenBudget: parsePositiveInt(env.HEADROOM_MAX_TOKEN_BUDGET, 'HEADROOM_MAX_TOKEN_BUDGET'), + containerRequestTimeoutMs: parsePositiveInt( + env.HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS, + 'HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS' + ), + }; +} + +function parseCsvSet(value: string): Set { + return new Set( + value + .split(',') + .map(part => part.trim()) + .filter(part => part.length > 0) + ); +} + +function parsePositiveInt(value: string, name: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + throw new ConfigError(`${name} must be a positive integer`); + } + return parsed; +} + +function parseModelLimits(value: string): ModelLimits { + const parsed: unknown = JSON.parse(value); + if (!isRecord(parsed) || !isRecord(parsed.context_limits)) { + throw new ConfigError('HEADROOM_MODEL_LIMITS must include context_limits'); + } + + const contextLimits: Record = {}; + for (const [model, limit] of Object.entries(parsed.context_limits)) { + if (typeof limit !== 'number' || !Number.isInteger(limit) || limit <= 0) { + throw new ConfigError(`HEADROOM_MODEL_LIMITS has invalid context limit for ${model}`); + } + contextLimits[model] = limit; + } + return { contextLimits }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/services/headroom-compress/src/guards.ts b/services/headroom-compress/src/guards.ts new file mode 100644 index 0000000000..3b2135c644 --- /dev/null +++ b/services/headroom-compress/src/guards.ts @@ -0,0 +1,283 @@ +import type { HeadroomRuntimeConfig } from './config'; + +export type CompressRequestBody = { + messages: unknown[]; + model: string; + token_budget?: number; + config?: { + compress_user_messages?: boolean; + target_ratio?: number; + protect_recent?: number; + protect_analysis_context?: boolean; + }; +}; + +export class HttpError extends Error { + constructor( + readonly status: number, + readonly code: string, + message: string + ) { + super(message); + } + + toBody(): { error: { type: string; message: string } } { + return { error: { type: this.code, message: this.message } }; + } +} + +export async function readJsonBodyWithLimit( + request: Request, + maxBytes: number +): Promise<{ json: unknown; byteLength: number }> { + const contentEncoding = request.headers.get('content-encoding'); + if (contentEncoding && contentEncoding.toLowerCase() !== 'identity') { + throw new HttpError( + 415, + 'unsupported_content_encoding', + 'Compressed request bodies are not accepted.' + ); + } + + const contentType = request.headers.get('content-type'); + if (!contentType?.toLowerCase().includes('application/json')) { + throw new HttpError(415, 'unsupported_media_type', 'Content-Type must be application/json.'); + } + + const contentLength = request.headers.get('content-length'); + if (contentLength !== null) { + const declaredLength = Number.parseInt(contentLength, 10); + if (!Number.isInteger(declaredLength) || declaredLength < 0) { + throw new HttpError( + 400, + 'invalid_content_length', + 'Content-Length must be a non-negative integer.' + ); + } + if (declaredLength > maxBytes) { + throw new HttpError(413, 'payload_too_large', 'Request body exceeds configured byte limit.'); + } + } + + const body = request.body; + if (!body) { + throw new HttpError(400, 'invalid_request', 'Request body is required.'); + } + + const reader = body.getReader(); + const chunks: Uint8Array[] = []; + let total = 0; + for (;;) { + const result: ReadableStreamReadResult = await reader.read(); + if (result.done) break; + const value = result.value; + total += value.byteLength; + if (total > maxBytes) { + throw new HttpError(413, 'payload_too_large', 'Request body exceeds configured byte limit.'); + } + chunks.push(value); + } + + const bytes = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { + bytes.set(chunk, offset); + offset += chunk.byteLength; + } + + const text = new TextDecoder().decode(bytes); + try { + return { json: JSON.parse(text), byteLength: total }; + } catch { + throw new HttpError(400, 'invalid_json', 'Request body must be valid JSON.'); + } +} + +export function validateCompressRequest( + value: unknown, + runtimeConfig: HeadroomRuntimeConfig +): CompressRequestBody { + if (!isRecord(value)) { + throw new HttpError(400, 'invalid_request', 'Request body must be a JSON object.'); + } + + const allowedTopLevelKeys = new Set(['messages', 'model', 'token_budget', 'config']); + for (const key of Object.keys(value)) { + if (!allowedTopLevelKeys.has(key)) { + throw new HttpError(400, 'invalid_request', `Unsupported field: ${key}.`); + } + } + + if (typeof value.model !== 'string' || value.model.trim() === '') { + throw new HttpError(400, 'invalid_model', 'model must be a non-empty string.'); + } + const model = value.model.trim(); + if (!runtimeConfig.modelAllowlist.has(model)) { + throw new HttpError(400, 'model_not_allowed', 'model is not enabled for Headroom compression.'); + } + + if (!Array.isArray(value.messages)) { + throw new HttpError(400, 'invalid_messages', 'messages must be an array.'); + } + if (value.messages.length > runtimeConfig.maxMessages) { + throw new HttpError( + 400, + 'too_many_messages', + 'messages exceeds configured message count limit.' + ); + } + for (const message of value.messages) { + validateMessage(message); + } + + const contentChars = countMessageContentChars(value.messages); + if (contentChars > runtimeConfig.maxContentChars) { + throw new HttpError( + 413, + 'payload_too_large', + 'message content exceeds configured character limit.' + ); + } + + const contextLimit = runtimeConfig.modelLimits.contextLimits[model]; + if (!Number.isInteger(contextLimit)) { + throw new HttpError(400, 'model_not_configured', 'model has no configured context limit.'); + } + + const body: CompressRequestBody = { messages: value.messages, model }; + const tokenBudget = value.token_budget; + if (tokenBudget !== undefined) { + if (typeof tokenBudget !== 'number' || !Number.isInteger(tokenBudget) || tokenBudget <= 0) { + throw new HttpError(400, 'invalid_token_budget', 'token_budget must be a positive integer.'); + } + if (tokenBudget > runtimeConfig.maxTokenBudget || tokenBudget > contextLimit) { + throw new HttpError(400, 'token_budget_too_large', 'token_budget exceeds configured limit.'); + } + body.token_budget = tokenBudget; + } + + if (value.config !== undefined) { + body.config = validateCompressConfig(value.config); + } + + return body; +} + +function validateMessage(value: unknown): void { + if (!isRecord(value)) { + throw new HttpError(400, 'invalid_messages', 'each message must be an object.'); + } + if (typeof value.role !== 'string' || value.role.trim() === '') { + throw new HttpError(400, 'invalid_messages', 'each message must include a string role.'); + } + if (!('content' in value)) { + throw new HttpError(400, 'invalid_messages', 'each message must include content.'); + } +} + +function validateCompressConfig(value: unknown): CompressRequestBody['config'] { + if (!isRecord(value)) { + throw new HttpError(400, 'invalid_config', 'config must be an object.'); + } + + const allowedConfigKeys = new Set([ + 'compress_user_messages', + 'target_ratio', + 'protect_recent', + 'protect_analysis_context', + ]); + for (const key of Object.keys(value)) { + if (!allowedConfigKeys.has(key)) { + throw new HttpError(400, 'invalid_config', `Unsupported config field: ${key}.`); + } + } + + const config: CompressRequestBody['config'] = {}; + if (value.compress_user_messages !== undefined) { + if (typeof value.compress_user_messages !== 'boolean') { + throw new HttpError( + 400, + 'invalid_config', + 'config.compress_user_messages must be a boolean.' + ); + } + config.compress_user_messages = value.compress_user_messages; + } + if (value.target_ratio !== undefined) { + if ( + typeof value.target_ratio !== 'number' || + value.target_ratio <= 0 || + value.target_ratio > 1 + ) { + throw new HttpError( + 400, + 'invalid_config', + 'config.target_ratio must be greater than 0 and at most 1.' + ); + } + config.target_ratio = value.target_ratio; + } + const protectRecent = value.protect_recent; + if (protectRecent !== undefined) { + if ( + typeof protectRecent !== 'number' || + !Number.isInteger(protectRecent) || + protectRecent < 0 || + protectRecent > 100 + ) { + throw new HttpError( + 400, + 'invalid_config', + 'config.protect_recent must be an integer from 0 to 100.' + ); + } + config.protect_recent = protectRecent; + } + if (value.protect_analysis_context !== undefined) { + if (typeof value.protect_analysis_context !== 'boolean') { + throw new HttpError( + 400, + 'invalid_config', + 'config.protect_analysis_context must be a boolean.' + ); + } + config.protect_analysis_context = value.protect_analysis_context; + } + + return config; +} + +function countMessageContentChars(messages: unknown[]): number { + let total = 0; + for (const message of messages) { + if (isRecord(message)) { + total += countChars(message.content, 0); + } + } + return total; +} + +function countChars(value: unknown, depth: number): number { + if (depth > 8) return 0; + if (typeof value === 'string') return value.length; + if (typeof value === 'number' || typeof value === 'boolean' || value === null) return 0; + if (Array.isArray(value)) { + let total = 0; + for (const item of value) { + total += countChars(item, depth + 1); + } + return total; + } + if (isRecord(value)) { + let total = 0; + for (const item of Object.values(value)) { + total += countChars(item, depth + 1); + } + return total; + } + return 0; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/services/headroom-compress/src/headroom-container.ts b/services/headroom-compress/src/headroom-container.ts new file mode 100644 index 0000000000..e8a222ad06 --- /dev/null +++ b/services/headroom-compress/src/headroom-container.ts @@ -0,0 +1,43 @@ +import { Container } from '@cloudflare/containers'; + +export class HeadroomContainer extends Container { + defaultPort = 8787; + sleepAfter = '10m'; + + envVars = { + HEADROOM_HOST: '0.0.0.0', + HEADROOM_PORT: '8787', + HEADROOM_WORKERS: '1', + HEADROOM_STATELESS: this.env.HEADROOM_STATELESS, + HEADROOM_TELEMETRY: this.env.HEADROOM_TELEMETRY, + HEADROOM_SKIP_UPSTREAM_CHECK: this.env.HEADROOM_SKIP_UPSTREAM_CHECK, + HEADROOM_NO_CCR_INJECT_TOOL: this.env.HEADROOM_NO_CCR_INJECT_TOOL, + HEADROOM_NO_CCR_MARKER: this.env.HEADROOM_NO_CCR_MARKER, + HEADROOM_NO_CCR_PROACTIVE_EXPANSION: this.env.HEADROOM_NO_CCR_PROACTIVE_EXPANSION, + HEADROOM_LOG_MESSAGES: this.env.HEADROOM_LOG_MESSAGES, + HEADROOM_RATE_LIMIT_ENABLED: this.env.HEADROOM_RATE_LIMIT_ENABLED, + HEADROOM_CODE_AWARE_ENABLED: this.env.HEADROOM_CODE_AWARE_ENABLED, + HEADROOM_COMPRESS_USER_MESSAGES: this.env.HEADROOM_COMPRESS_USER_MESSAGES, + HEADROOM_COMPRESS_SYSTEM_MESSAGES: this.env.HEADROOM_COMPRESS_SYSTEM_MESSAGES, + HEADROOM_PROTECT_RECENT: this.env.HEADROOM_PROTECT_RECENT, + HEADROOM_LIMIT_CONCURRENCY: this.env.HEADROOM_LIMIT_CONCURRENCY, + HEADROOM_COMPRESS_WORKERS: this.env.HEADROOM_COMPRESS_WORKERS, + HEADROOM_KOMPRESS_MAX_CONCURRENT: this.env.HEADROOM_KOMPRESS_MAX_CONCURRENT, + HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM: + this.env.HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM, + HEADROOM_MODEL_LIMITS: this.env.HEADROOM_MODEL_LIMITS, + }; + + override onStart(): void { + console.log(JSON.stringify({ event: 'headroom_container_started' })); + } + + override onError(error: unknown): void { + console.error( + JSON.stringify({ + event: 'headroom_container_error', + error: error instanceof Error ? error.message : String(error), + }) + ); + } +} diff --git a/services/headroom-compress/src/hono-env.ts b/services/headroom-compress/src/hono-env.ts new file mode 100644 index 0000000000..465d632911 --- /dev/null +++ b/services/headroom-compress/src/hono-env.ts @@ -0,0 +1,3 @@ +export type HonoEnv = { + Bindings: Env; +}; diff --git a/services/headroom-compress/src/index.test.ts b/services/headroom-compress/src/index.test.ts new file mode 100644 index 0000000000..ef5d958c1f --- /dev/null +++ b/services/headroom-compress/src/index.test.ts @@ -0,0 +1,185 @@ +import { app } from './index'; + +const model = 'kilo/anthropic/claude-sonnet-4.6'; + +type CapturedRequest = { + request: Request; + body: unknown; +}; + +function makeEnv( + options: { token?: string; fetch?: (request: Request) => Promise } = {} +) { + const captured: CapturedRequest[] = []; + const token = options.token ?? 'secret-token'; + const fetch = + options.fetch ?? + (async (request: Request) => { + captured.push({ request, body: await request.clone().json() }); + return Response.json({ + messages: [{ role: 'user', content: 'compressed' }], + tokens_before: 10, + tokens_after: 5, + tokens_saved: 5, + compression_ratio: 0.5, + transforms_applied: ['test'], + transforms_summary: { test: 1 }, + ccr_hashes: ['abc'], + }); + }); + + const env = { + HEADROOM_BEARER_TOKEN: { get: async () => token }, + HEADROOM_CONTAINER: { + idFromName: (name: string) => ({ name }), + get: () => ({ fetch }), + }, + HEADROOM_INSTANCE_COUNT: '2', + HEADROOM_MODEL_ALLOWLIST: model, + HEADROOM_MODEL_LIMITS: JSON.stringify({ context_limits: { [model]: 1_000_000 } }), + HEADROOM_MAX_BODY_BYTES: '1048576', + HEADROOM_MAX_MESSAGES: '200', + HEADROOM_MAX_CONTENT_CHARS: '750000', + HEADROOM_MAX_TOKEN_BUDGET: '256000', + HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS: '25000', + ENVIRONMENT: 'production', + HEADROOM_STATELESS: 'true', + HEADROOM_TELEMETRY: 'off', + HEADROOM_SKIP_UPSTREAM_CHECK: '1', + HEADROOM_NO_CCR_INJECT_TOOL: '1', + HEADROOM_NO_CCR_MARKER: '1', + HEADROOM_NO_CCR_PROACTIVE_EXPANSION: '1', + HEADROOM_LOG_MESSAGES: 'false', + HEADROOM_RATE_LIMIT_ENABLED: 'false', + HEADROOM_CODE_AWARE_ENABLED: '1', + HEADROOM_COMPRESS_USER_MESSAGES: '1', + HEADROOM_COMPRESS_SYSTEM_MESSAGES: '1', + HEADROOM_PROTECT_RECENT: '0', + HEADROOM_LIMIT_CONCURRENCY: '8', + HEADROOM_COMPRESS_WORKERS: '4', + HEADROOM_KOMPRESS_MAX_CONCURRENT: '2', + HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM: '2', + } as unknown as Env; + + return { env, captured }; +} + +function compressRequest(body: unknown, token = 'secret-token'): Request { + return new Request('https://headroom.kiloapps.io/v1/compress', { + method: 'POST', + headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('headroom-compress worker', () => { + it('serves readyz without waking a container', async () => { + let containerCalls = 0; + const { env } = makeEnv({ + fetch: async () => { + containerCalls += 1; + return Response.json({}); + }, + }); + + const response = await app.request('/readyz', {}, env); + + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ status: 'ok', service: 'headroom-compress' }); + expect(containerCalls).toBe(0); + }); + + it('requires bearer auth for compression', async () => { + const { env } = makeEnv(); + const response = await app.request( + '/v1/compress', + { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ model, messages: [{ role: 'user', content: 'hello' }] }), + }, + env + ); + + expect(response.status).toBe(401); + }); + + it('forwards valid compression requests and strips authorization', async () => { + const { env, captured } = makeEnv(); + const response = await app.fetch( + compressRequest({ + model, + messages: [{ role: 'user', content: 'hello' }], + config: { compress_user_messages: true, target_ratio: 0.5 }, + }), + env + ); + + expect(response.status).toBe(200); + await expect(response.json()).resolves.toMatchObject({ ccr_hashes: ['abc'] }); + expect(captured).toHaveLength(1); + expect(captured[0].request.headers.get('authorization')).toBeNull(); + expect(captured[0].request.headers.get('x-request-id')).toBeTruthy(); + expect(captured[0].body).toMatchObject({ + model, + config: { compress_user_messages: true, target_ratio: 0.5 }, + }); + }); + + it('rejects disallowed Headroom routes before container fetch', async () => { + let containerCalls = 0; + const { env } = makeEnv({ + fetch: async () => { + containerCalls += 1; + return Response.json({}); + }, + }); + + const response = await app.request( + '/v1/chat/completions', + { + method: 'POST', + headers: { authorization: 'Bearer secret-token', 'content-type': 'application/json' }, + body: '{}', + }, + env + ); + + expect(response.status).toBe(404); + expect(containerCalls).toBe(0); + }); + + it('rejects non-allowlisted models', async () => { + const { env } = makeEnv(); + const response = await app.fetch( + compressRequest({ + model: 'kilo/openai/not-enabled', + messages: [{ role: 'user', content: 'hello' }], + }), + env + ); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + error: { type: 'model_not_allowed' }, + }); + }); + + it('rejects oversized declared content length', async () => { + const { env } = makeEnv(); + const response = await app.fetch( + new Request('https://headroom.kiloapps.io/v1/compress', { + method: 'POST', + headers: { + authorization: 'Bearer secret-token', + 'content-type': 'application/json', + 'content-length': '1048577', + }, + body: JSON.stringify({ model, messages: [{ role: 'user', content: 'hello' }] }), + }), + env + ); + + expect(response.status).toBe(413); + }); +}); diff --git a/services/headroom-compress/src/index.ts b/services/headroom-compress/src/index.ts new file mode 100644 index 0000000000..e0a4473b88 --- /dev/null +++ b/services/headroom-compress/src/index.ts @@ -0,0 +1,195 @@ +import { Hono } from 'hono'; +import { createErrorHandler } from '@kilocode/worker-utils'; +import { authMiddleware } from './auth'; +import { ConfigError, loadConfig } from './config'; +import { HttpError, readJsonBodyWithLimit, validateCompressRequest } from './guards'; +import type { CompressRequestBody } from './guards'; +import type { HonoEnv } from './hono-env'; + +export { HeadroomContainer } from './headroom-container'; + +const SERVICE = 'headroom-compress'; + +export const app = new Hono(); + +app.get('/readyz', c => { + try { + const config = loadConfig(c.env); + return c.json({ + status: 'ok', + service: SERVICE, + models: config.modelAllowlist.size, + instances: config.instanceCount, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(JSON.stringify({ event: 'headroom_readyz_config_error', error: message })); + return jsonResponse(500, { + error: { type: 'configuration_error', message: 'Headroom Worker is misconfigured.' }, + }); + } +}); + +app.post('/v1/compress', authMiddleware, async c => { + const requestId = getRequestId(c.req.raw); + const startedAt = Date.now(); + + try { + const config = loadConfig(c.env); + const { json, byteLength } = await readJsonBodyWithLimit(c.req.raw, config.maxBodyBytes); + const body = validateCompressRequest(json, config); + const containerIndex = randomInstanceIndex(config.instanceCount); + const response = await forwardCompressRequest({ + env: c.env, + body, + requestId, + containerIndex, + timeoutMs: config.containerRequestTimeoutMs, + }); + + console.log( + JSON.stringify({ + event: 'headroom_compress_completed', + request_id: requestId, + model: body.model, + request_bytes: byteLength, + status: response.status, + duration_ms: Date.now() - startedAt, + container_index: containerIndex, + }) + ); + + return filterContainerResponse(response, requestId); + } catch (error) { + return handleCompressError(error, requestId, startedAt); + } +}); + +app.all('*', c => { + console.log( + JSON.stringify({ + event: 'headroom_route_rejected', + method: c.req.method, + path: new URL(c.req.url).pathname, + }) + ); + return jsonResponse(404, { error: { type: 'not_found', message: 'Not found.' } }); +}); + +app.onError(createErrorHandler(console, { includeMessage: false })); + +export default { fetch: app.fetch }; + +type ForwardArgs = { + env: Env; + body: CompressRequestBody; + requestId: string; + containerIndex: number; + timeoutMs: number; +}; + +async function forwardCompressRequest({ + env, + body, + requestId, + containerIndex, + timeoutMs, +}: ForwardArgs): Promise { + const containerId = env.HEADROOM_CONTAINER.idFromName(`headroom-${containerIndex}`); + const container = env.HEADROOM_CONTAINER.get(containerId); + const request = new Request('http://headroom.local/v1/compress', { + method: 'POST', + headers: { + accept: 'application/json', + 'content-type': 'application/json', + 'x-request-id': requestId, + }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(timeoutMs), + }); + + return await container.fetch(request); +} + +function filterContainerResponse(response: Response, requestId: string): Response { + const headers = new Headers(); + headers.set('cache-control', 'no-store'); + headers.set('x-request-id', requestId); + const contentType = response.headers.get('content-type'); + if (contentType) { + headers.set('content-type', contentType); + } + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); +} + +function handleCompressError(error: unknown, requestId: string, startedAt: number): Response { + if (error instanceof HttpError) { + console.warn( + JSON.stringify({ + event: 'headroom_compress_rejected', + request_id: requestId, + status: error.status, + type: error.code, + duration_ms: Date.now() - startedAt, + }) + ); + return jsonResponse(error.status, error.toBody(), requestId); + } + + if (error instanceof ConfigError) { + console.error( + JSON.stringify({ + event: 'headroom_compress_config_error', + request_id: requestId, + error: error.message, + }) + ); + return jsonResponse( + 500, + { error: { type: 'configuration_error', message: 'Headroom Worker is misconfigured.' } }, + requestId + ); + } + + const isTimeout = + error instanceof DOMException && (error.name === 'TimeoutError' || error.name === 'AbortError'); + const status = isTimeout ? 504 : 502; + const type = isTimeout ? 'container_timeout' : 'container_error'; + const message = isTimeout + ? 'Headroom compression timed out.' + : 'Headroom container request failed.'; + console.error( + JSON.stringify({ + event: 'headroom_compress_failed', + request_id: requestId, + type, + error: error instanceof Error ? error.message : String(error), + duration_ms: Date.now() - startedAt, + }) + ); + return jsonResponse(status, { error: { type, message } }, requestId); +} + +function jsonResponse(status: number, body: unknown, requestId?: string): Response { + const headers = new Headers({ 'content-type': 'application/json', 'cache-control': 'no-store' }); + if (requestId) { + headers.set('x-request-id', requestId); + } + return new Response(JSON.stringify(body), { status, headers }); +} + +function getRequestId(request: Request): string { + return ( + request.headers.get('x-request-id') ?? request.headers.get('cf-ray') ?? crypto.randomUUID() + ); +} + +function randomInstanceIndex(instanceCount: number): number { + const value = new Uint32Array(1); + crypto.getRandomValues(value); + return value[0] % instanceCount; +} diff --git a/services/headroom-compress/test/stubs/cloudflare-containers.ts b/services/headroom-compress/test/stubs/cloudflare-containers.ts new file mode 100644 index 0000000000..d2b754e248 --- /dev/null +++ b/services/headroom-compress/test/stubs/cloudflare-containers.ts @@ -0,0 +1,16 @@ +export class Container { + defaultPort?: number; + sleepAfter?: string; + env: Env; + + constructor(_ctx: unknown, env: Env) { + this.env = env; + } + + fetch(request: Request): Response { + return new Response(null, { + status: 502, + statusText: `Container stub cannot handle ${request.method} ${new URL(request.url).pathname}`, + }); + } +} diff --git a/services/headroom-compress/tsconfig.json b/services/headroom-compress/tsconfig.json new file mode 100644 index 0000000000..5157de404b --- /dev/null +++ b/services/headroom-compress/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "types": [ + "@types/node", + "@cloudflare/workers-types", + "vitest/globals", + "./worker-configuration.d.ts" + ], + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["worker-configuration.d.ts", "src/**/*.ts", "src/**/*.d.ts", "vitest.config.ts"] +} diff --git a/services/headroom-compress/vitest.config.ts b/services/headroom-compress/vitest.config.ts new file mode 100644 index 0000000000..ae9a95b5d1 --- /dev/null +++ b/services/headroom-compress/vitest.config.ts @@ -0,0 +1,15 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@cloudflare/containers': resolve(__dirname, 'test/stubs/cloudflare-containers.ts'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/services/headroom-compress/worker-configuration.d.ts b/services/headroom-compress/worker-configuration.d.ts new file mode 100644 index 0000000000..67e67d8a3c --- /dev/null +++ b/services/headroom-compress/worker-configuration.d.ts @@ -0,0 +1,45 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false --env-file /dev/null` (hash: 80e3fd6cbec195713d6bf5b4cfdc702d) +interface __BaseEnv_Env { + HEADROOM_BEARER_TOKEN: SecretsStoreSecret; + ENVIRONMENT: "production"; + HEADROOM_INSTANCE_COUNT: "2"; + HEADROOM_MODEL_ALLOWLIST: "kilo/poolside/laguna-m.1:free,kilo/stepfun/step-3.7-flash:free,kilo/anthropic/claude-opus-4.8,kilo/stealth/claude-opus-4.8,kilo/stealth/claude-opus-4.7,kilo/stealth/claude-sonnet-4.6,kilo/stealth/claude-opus-4.6,kilo/moonshotai/kimi-k2.7-code,kilo/anthropic/claude-sonnet-4.6,kilo/openai/gpt-5.5,kilo/google/gemini-3.1-pro-preview,kilo/minimax/minimax-m3,kilo/qwen/qwen3.7-plus,kilo/stealth/qwen3.6-plus,kilo/z-ai/glm-5.2"; + HEADROOM_MODEL_LIMITS: "{\"context_limits\":{\"kilo/poolside/laguna-m.1:free\":262144,\"kilo/stepfun/step-3.7-flash:free\":262144,\"kilo/anthropic/claude-opus-4.8\":1000000,\"kilo/stealth/claude-opus-4.8\":1000000,\"kilo/stealth/claude-opus-4.7\":1000000,\"kilo/stealth/claude-sonnet-4.6\":1000000,\"kilo/stealth/claude-opus-4.6\":1000000,\"kilo/moonshotai/kimi-k2.7-code\":262144,\"kilo/anthropic/claude-sonnet-4.6\":1000000,\"kilo/openai/gpt-5.5\":1050000,\"kilo/google/gemini-3.1-pro-preview\":1048576,\"kilo/minimax/minimax-m3\":1048576,\"kilo/qwen/qwen3.7-plus\":1000000,\"kilo/stealth/qwen3.6-plus\":1000000,\"kilo/z-ai/glm-5.2\":1048576}}"; + HEADROOM_MAX_BODY_BYTES: "1048576"; + HEADROOM_MAX_MESSAGES: "200"; + HEADROOM_MAX_CONTENT_CHARS: "750000"; + HEADROOM_MAX_TOKEN_BUDGET: "256000"; + HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS: "25000"; + HEADROOM_STATELESS: "true"; + HEADROOM_TELEMETRY: "off"; + HEADROOM_SKIP_UPSTREAM_CHECK: "1"; + HEADROOM_NO_CCR_INJECT_TOOL: "1"; + HEADROOM_NO_CCR_MARKER: "1"; + HEADROOM_NO_CCR_PROACTIVE_EXPANSION: "1"; + HEADROOM_LOG_MESSAGES: "false"; + HEADROOM_RATE_LIMIT_ENABLED: "false"; + HEADROOM_CODE_AWARE_ENABLED: "1"; + HEADROOM_COMPRESS_USER_MESSAGES: "1"; + HEADROOM_COMPRESS_SYSTEM_MESSAGES: "1"; + HEADROOM_PROTECT_RECENT: "0"; + HEADROOM_LIMIT_CONCURRENCY: "8"; + HEADROOM_COMPRESS_WORKERS: "4"; + HEADROOM_KOMPRESS_MAX_CONCURRENT: "2"; + HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM: "2"; + HEADROOM_CONTAINER: DurableObjectNamespace; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + durableNamespaces: "HeadroomContainer"; + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} +type StringifyValues> = { + [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; +}; +declare namespace NodeJS { + interface ProcessEnv extends StringifyValues> {} +} diff --git a/services/headroom-compress/wrangler.jsonc b/services/headroom-compress/wrangler.jsonc new file mode 100644 index 0000000000..776a56eec5 --- /dev/null +++ b/services/headroom-compress/wrangler.jsonc @@ -0,0 +1,83 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "account_id": "e115e769bcdd4c3d66af59d3332cb394", + "name": "headroom-compress", + "main": "src/index.ts", + "compatibility_date": "2026-06-23", + "compatibility_flags": ["nodejs_compat"], + "workers_dev": false, + "preview_urls": false, + "logpush": true, + "placement": { "mode": "smart" }, + "upload_source_maps": true, + "routes": [ + { + "pattern": "headroom.kiloapps.io", + "zone_name": "kiloapps.io", + "custom_domain": true, + }, + ], + "observability": { + "enabled": true, + "head_sampling_rate": 1, + "logs": { + "enabled": true, + "head_sampling_rate": 1, + "persist": true, + "invocation_logs": true, + }, + "traces": { + "enabled": true, + "persist": true, + "head_sampling_rate": 1, + }, + }, + "vars": { + "ENVIRONMENT": "production", + "HEADROOM_INSTANCE_COUNT": "2", + "HEADROOM_MODEL_ALLOWLIST": "kilo/poolside/laguna-m.1:free,kilo/stepfun/step-3.7-flash:free,kilo/anthropic/claude-opus-4.8,kilo/stealth/claude-opus-4.8,kilo/stealth/claude-opus-4.7,kilo/stealth/claude-sonnet-4.6,kilo/stealth/claude-opus-4.6,kilo/moonshotai/kimi-k2.7-code,kilo/anthropic/claude-sonnet-4.6,kilo/openai/gpt-5.5,kilo/google/gemini-3.1-pro-preview,kilo/minimax/minimax-m3,kilo/qwen/qwen3.7-plus,kilo/stealth/qwen3.6-plus,kilo/z-ai/glm-5.2", + "HEADROOM_MODEL_LIMITS": "{\"context_limits\":{\"kilo/poolside/laguna-m.1:free\":262144,\"kilo/stepfun/step-3.7-flash:free\":262144,\"kilo/anthropic/claude-opus-4.8\":1000000,\"kilo/stealth/claude-opus-4.8\":1000000,\"kilo/stealth/claude-opus-4.7\":1000000,\"kilo/stealth/claude-sonnet-4.6\":1000000,\"kilo/stealth/claude-opus-4.6\":1000000,\"kilo/moonshotai/kimi-k2.7-code\":262144,\"kilo/anthropic/claude-sonnet-4.6\":1000000,\"kilo/openai/gpt-5.5\":1050000,\"kilo/google/gemini-3.1-pro-preview\":1048576,\"kilo/minimax/minimax-m3\":1048576,\"kilo/qwen/qwen3.7-plus\":1000000,\"kilo/stealth/qwen3.6-plus\":1000000,\"kilo/z-ai/glm-5.2\":1048576}}", + "HEADROOM_MAX_BODY_BYTES": "1048576", + "HEADROOM_MAX_MESSAGES": "200", + "HEADROOM_MAX_CONTENT_CHARS": "750000", + "HEADROOM_MAX_TOKEN_BUDGET": "256000", + "HEADROOM_CONTAINER_REQUEST_TIMEOUT_MS": "25000", + "HEADROOM_STATELESS": "true", + "HEADROOM_TELEMETRY": "off", + "HEADROOM_SKIP_UPSTREAM_CHECK": "1", + "HEADROOM_NO_CCR_INJECT_TOOL": "1", + "HEADROOM_NO_CCR_MARKER": "1", + "HEADROOM_NO_CCR_PROACTIVE_EXPANSION": "1", + "HEADROOM_LOG_MESSAGES": "false", + "HEADROOM_RATE_LIMIT_ENABLED": "false", + "HEADROOM_CODE_AWARE_ENABLED": "1", + "HEADROOM_COMPRESS_USER_MESSAGES": "1", + "HEADROOM_COMPRESS_SYSTEM_MESSAGES": "1", + "HEADROOM_PROTECT_RECENT": "0", + "HEADROOM_LIMIT_CONCURRENCY": "8", + "HEADROOM_COMPRESS_WORKERS": "4", + "HEADROOM_KOMPRESS_MAX_CONCURRENT": "2", + "HEADROOM_TOOL_OUTPUT_COMPRESSION_PARALLELISM": "2", + }, + "containers": [ + { + "name": "headroom-compress", + "class_name": "HeadroomContainer", + "image": "registry.cloudflare.com/e115e769bcdd4c3d66af59d3332cb394/headroom-compress:0.27.0-ghcr9f5f0de", + "instance_type": "standard-4", + "max_instances": 10, + }, + ], + "durable_objects": { + "bindings": [{ "name": "HEADROOM_CONTAINER", "class_name": "HeadroomContainer" }], + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["HeadroomContainer"] }], + "secrets_store_secrets": [ + { + "binding": "HEADROOM_BEARER_TOKEN", + "store_id": "342a86d9e3a94da698e82d0c6e2a36f0", + "secret_name": "HEADROOM_BEARER_TOKEN", + }, + ], + "dev": { "port": 8815, "local_protocol": "http", "ip": "0.0.0.0" }, +}