From 762b63ba9a9aa29416531376c1e08e3851212118 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Fri, 7 Aug 2026 17:06:18 +0530 Subject: [PATCH 1/3] auto mode --- CHANGELOG.md | 11 + package-lock.json | 573 ++++++------------------------ package.json | 3 +- src/api/contextWindowGuard.ts | 396 +++++++++++++++++++++ src/api/models.ts | 33 +- src/api/provider.ts | 7 +- src/ui/components/ModelPicker.tsx | 4 +- test/axon-auto-models.test.ts | 29 ++ test/context-window-guard.test.ts | 212 +++++++++++ 9 files changed, 792 insertions(+), 476 deletions(-) create mode 100644 src/api/contextWindowGuard.ts create mode 100644 test/axon-auto-models.test.ts create mode 100644 test/context-window-guard.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c8426bc..e5a1e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Added a final context-window guard around every Axon and AI SDK request, + including normal agent turns, retries, and `/compact`. The guard reserves the + selected model's full output allowance and a safety margin, counts system + prompts, history, tool results, tool arguments, images, and native/MCP tool + schemas, then safely compacts oversized input or stops locally before making + an unsafe provider call. + ## [6.7.4] - 2026-08-04 ### Added diff --git a/package-lock.json b/package-lock.json index 7c8e7d8..00a2cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,28 +1,26 @@ { "name": "@matterailab/orbcode", - "version": "0.6.0", + "version": "6.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@matterailab/orbcode", - "version": "0.6.0", + "version": "6.7.4", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^3.0.85", "@ai-sdk/openai-compatible": "^2.0.51", + "@firecrawl/anydoc": "^0.1.6", "@modelcontextprotocol/sdk": "^1.29.0", "@opentui/core": "0.4.3", "@opentui/react": "0.4.3", "ai": "^6.0.207", "bun": "1.3.14", - "mammoth": "^1.12.0", "open": "^10.1.0", "openai": "^4.78.0", - "pdf-parse": "^2.4.5", "picomatch": "4.0.4", - "react": "^19.2.0", - "read-excel-file": "^9.2.0" + "react": "^19.2.0" }, "bin": { "orbcode": "bin/orbcode.js" @@ -725,102 +723,31 @@ "@ff-labs/fff-bin-win32-x64": "0.10.1" } }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "node_modules/@firecrawl/anydoc": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc/-/anydoc-0.1.6.tgz", + "integrity": "sha512-zA7xwFn7MHvdb6FOPoRJu3oG0A6sP7zyFIKvp7sGptKfTyU1oqP0yNiAT9oOrRYBvS0gAUHkrfOClplGZXLM7g==", "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" + "bin": { + "anydoc": "cli.js" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", - "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", - "license": "MIT", - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.80", - "@napi-rs/canvas-darwin-arm64": "0.1.80", - "@napi-rs/canvas-darwin-x64": "0.1.80", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", - "@napi-rs/canvas-linux-arm64-musl": "0.1.80", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-musl": "0.1.80", - "@napi-rs/canvas-win32-x64-msvc": "0.1.80" - } - }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", - "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" + "@firecrawl/anydoc-darwin-arm64": "0.1.6", + "@firecrawl/anydoc-darwin-x64": "0.1.6", + "@firecrawl/anydoc-linux-arm64-gnu": "0.1.6", + "@firecrawl/anydoc-linux-arm64-musl": "0.1.6", + "@firecrawl/anydoc-linux-x64-gnu": "0.1.6", + "@firecrawl/anydoc-linux-x64-musl": "0.1.6", + "@firecrawl/anydoc-win32-x64-msvc": "0.1.6" } }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", - "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "node_modules/@firecrawl/anydoc-darwin-arm64": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-darwin-arm64/-/anydoc-darwin-arm64-0.1.6.tgz", + "integrity": "sha512-2745ZMZ6mPv2ak9j/sz6DDj4+hC9Eg1Lsipya9bHJMm8/WIOtgBF1XW04qKdFEWJQB5VdQ0o1J1tHaEyoUcIpQ==", "cpu": [ "arm64" ], @@ -830,13 +757,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", - "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "node_modules/@firecrawl/anydoc-darwin-x64": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-darwin-x64/-/anydoc-darwin-x64-0.1.6.tgz", + "integrity": "sha512-kGM0V5mcv/Ut2kVvtWUjH/iPXHdMwUbn4Kyh85lCm6FCgeieD+GYXVREdI3nPUEGMZGdX5i3uEh5sPyIYqGNYA==", "cpu": [ "x64" ], @@ -846,29 +773,13 @@ "darwin" ], "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", - "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", - "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "node_modules/@firecrawl/anydoc-linux-arm64-gnu": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-arm64-gnu/-/anydoc-linux-arm64-gnu-0.1.6.tgz", + "integrity": "sha512-ZHA7gNIltemhC6dEMnT2fc49Lo9ucpw6WsxtHBfXgKAZ9beZ5lDMzhZgaFpKSlv+vcnK7cFlOIg5RtRSuZjMlQ==", "cpu": [ "arm64" ], @@ -881,13 +792,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", - "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "node_modules/@firecrawl/anydoc-linux-arm64-musl": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-arm64-musl/-/anydoc-linux-arm64-musl-0.1.6.tgz", + "integrity": "sha512-mnfxDROQ4JWViWOh/58CK+RZBKoUwA4+e02ctUaPqMiBB4tXuxb5s8b5zr3J6ySgvoqe8qJ9UaTcOru1kY0sKg==", "cpu": [ "arm64" ], @@ -900,15 +811,15 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", - "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "node_modules/@firecrawl/anydoc-linux-x64-gnu": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-x64-gnu/-/anydoc-linux-x64-gnu-0.1.6.tgz", + "integrity": "sha512-bQLhBBeBuNQeRDORwkhUSYjt0+oPYtBt5eLZ5wLf8xFLJmor6e1/yx8CiJBqD93bYbpfqh0r8v6sb+pVsVCgCw==", "cpu": [ - "riscv64" + "x64" ], "libc": [ "glibc" @@ -919,18 +830,18 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", - "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "node_modules/@firecrawl/anydoc-linux-x64-musl": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-x64-musl/-/anydoc-linux-x64-musl-0.1.6.tgz", + "integrity": "sha512-W/kV3UBBJQ0vPH7ph2UX0XBJq5Qv0L33aZnYw1yFJ5RSSd/PGxSO6xEsyXsOib4SQ7APUq3fp6P5Drj5elfG1Q==", "cpu": [ "x64" ], "libc": [ - "glibc" + "musl" ], "license": "MIT", "optional": true, @@ -938,42 +849,75 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", - "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "node_modules/@firecrawl/anydoc-win32-x64-msvc": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-win32-x64-msvc/-/anydoc-win32-x64-msvc-0.1.6.tgz", + "integrity": "sha512-HKcG9YUSb7m9QfwXRaYpgGMkkkByM2dY/KGqFwQJRLtSoMGnQ9pYZSeqRVXsfG+5YXixVTfO4l4Zph54G2RQmg==", "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", - "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", - "cpu": [ - "x64" - ], + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">= 10" + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@opentelemetry/api": { @@ -1587,15 +1531,6 @@ "win32" ] }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", - "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@yuuang/ffi-rs-android-arm64": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-android-arm64/-/ffi-rs-android-arm64-1.3.2.tgz", @@ -1910,47 +1845,12 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2142,12 +2042,6 @@ "node": ">=6.6.0" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2270,21 +2164,6 @@ "node": ">=0.3.1" } }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", - "license": "BSD-2-Clause" - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "license": "BSD", - "dependencies": { - "underscore": "^1.13.1" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2580,12 +2459,6 @@ "@yuuang/ffi-rs-win32-x64-msvc": "1.3.2" } }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "license": "MIT" - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2741,12 +2614,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2840,12 +2707,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2924,12 +2785,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2963,92 +2818,6 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/lop": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", - "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", - "license": "BSD-2-Clause", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/mammoth": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", - "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", - "license": "BSD-2-Clause", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.2", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/marked": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", @@ -3147,12 +2916,6 @@ } } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3258,18 +3021,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", - "license": "BSD-2-Clause" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3279,15 +3030,6 @@ "node": ">= 0.8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3307,38 +3049,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/pdf-parse": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", - "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", - "license": "Apache-2.0", - "dependencies": { - "@napi-rs/canvas": "0.1.80", - "pdfjs-dist": "5.4.296" - }, - "bin": { - "pdf-parse": "bin/cli.mjs" - }, - "engines": { - "node": ">=20.16.0 <21 || >=22.3.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/mehmet-kozan" - } - }, - "node_modules/pdfjs-dist": { - "version": "5.4.296", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", - "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.80" - } - }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -3360,12 +3070,6 @@ "node": ">=16.20.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3481,29 +3185,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/read-excel-file": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-9.2.0.tgz", - "integrity": "sha512-jwSi8Q9oCmswrYMkuiKvJ6VdHbAXGQnANSOaHBNQZEm2XvaXx8d0t/V7oepgHqT9YWxlQE3HEuCtDZq8BZBmvw==", - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.9.10", - "fflate": "^0.8.3", - "unzipper-esm": "^0.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/read-excel-file/node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3617,12 +3298,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3735,12 +3410,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3885,12 +3554,6 @@ "node": ">=14.17" } }, - "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -3906,25 +3569,6 @@ "node": ">= 0.8" } }, - "node_modules/unzipper-esm": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.2.tgz", - "integrity": "sha512-lt8GtgDYV8YcAFZNQuLyR2QvHI8C/TstpgsdjUn9ZxiWLJgn+e5uW6DsO3e/HUJVuWD57ZLLFMZ9xk26tePuHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.2", - "node-int64": "^0.4.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4023,15 +3667,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index f2b236f..335bedc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matterailab/orbcode", - "version": "6.7.4", + "version": "6.7.6", "description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI", "type": "module", "bin": { @@ -34,6 +34,7 @@ "dev": "bun src/index.tsx", "start": "bun dist/index.js", "test:attachments": "node --import tsx --test test/attachments.test.ts", + "test:context": "node --import tsx --test test/context-window-guard.test.ts", "test:mcp": "node --import tsx --test test/mcp-client.test.ts", "test:plugins": "node --import tsx --test test/plugins.test.ts", "test:metrics": "node --import tsx --test test/metrics.test.ts", diff --git a/src/api/contextWindowGuard.ts b/src/api/contextWindowGuard.ts new file mode 100644 index 0000000..d2eb72b --- /dev/null +++ b/src/api/contextWindowGuard.ts @@ -0,0 +1,396 @@ +import type OpenAI from "openai" + +import type { LLMClient } from "./llmClient.js" +import type { AxonModel } from "./models.js" +import type { ApiStreamChunk } from "./stream.js" + +export const CONTEXT_WINDOW_SAFETY_PERCENTAGE = 0.1 + +const REQUEST_OVERHEAD_TOKENS = 512 +const MESSAGE_OVERHEAD_TOKENS = 32 +const TOOL_OVERHEAD_TOKENS = 32 +const IMAGE_MINIMUM_TOKENS = 16_384 +const REMOTE_IMAGE_TOKENS = 64_000 +const TRUNCATION_MARKER = "\n[...content truncated to fit the selected model's context window...]\n" +const HISTORY_OMISSION_MARKER = + "[Earlier conversation turns were omitted by OrbCode's context-window guard before this request.]" + +type Message = OpenAI.Chat.ChatCompletionMessageParam +type ToolDefinition = OpenAI.Chat.ChatCompletionTool +type MutableMessage = Message & Record + +export interface PreparedContextWindowRequest { + systemPrompt: string + messages: Message[] + tools: ToolDefinition[] + contextWindow: number + outputTokenReserve: number + inputTokenBudget: number + estimatedInputTokens: number + originalEstimatedInputTokens: number + wasTruncated: boolean +} + +interface MutableRequest { + systemPrompt: string + messages: MutableMessage[] + tools: ToolDefinition[] +} + +/** Install one final request boundary around either OrbCode transport. */ +export class ContextWindowGuardClient implements LLMClient { + constructor( + private readonly delegate: LLMClient, + private readonly model: AxonModel, + ) {} + + async *createMessage( + systemPrompt: string, + messages: Message[], + tools: ToolDefinition[], + abortSignal?: AbortSignal, + ): AsyncGenerator { + const prepared = prepareContextWindowRequest({ + model: this.model, + systemPrompt, + messages, + tools, + }) + + yield* this.delegate.createMessage( + prepared.systemPrompt, + prepared.messages, + prepared.tools, + abortSignal, + ) + } +} + +export function prepareContextWindowRequest({ + model, + systemPrompt, + messages, + tools, +}: { + model: AxonModel + systemPrompt: string + messages: Message[] + tools: ToolDefinition[] +}): PreparedContextWindowRequest { + const contextWindow = validateTokenLimit(model.contextWindow, "context window") + const outputTokenReserve = validateTokenLimit(model.maxOutputTokens, "maximum output") + const inputTokenBudget = Math.floor( + contextWindow * (1 - CONTEXT_WINDOW_SAFETY_PERCENTAGE) - outputTokenReserve, + ) + + if (inputTokenBudget <= REQUEST_OVERHEAD_TOKENS) { + throw contextWindowError(model, REQUEST_OVERHEAD_TOKENS, inputTokenBudget) + } + + let request: MutableRequest = { + systemPrompt, + messages: messages.map(cloneMessage), + tools: tools.map(cloneTool), + } + const originalEstimatedInputTokens = estimateContextWindowInput(request) + let estimatedInputTokens = originalEstimatedInputTokens + + if (estimatedInputTokens <= inputTokenBudget) { + return { + systemPrompt: request.systemPrompt, + messages: request.messages, + tools: request.tools, + contextWindow, + outputTokenReserve, + inputTokenBudget, + estimatedInputTokens, + originalEstimatedInputTokens, + wasTruncated: false, + } + } + + for (let round = 0; round < 6 && estimatedInputTokens > inputTokenBudget; round++) { + const ratio = Math.max(0.01, Math.min(0.85, (inputTokenBudget / estimatedInputTokens) * 0.82)) + request = shrinkRequest(request, ratio, round) + estimatedInputTokens = estimateContextWindowInput(request) + } + + while (estimatedInputTokens > inputTokenBudget && request.messages.length > 4) { + const shortened = dropOldestConversationPrefix(request.messages) + if (shortened.length === request.messages.length) break + request.messages = shortened + estimatedInputTokens = estimateContextWindowInput(request) + } + + if (estimatedInputTokens > inputTokenBudget) { + request = aggressivelyCompactRequest(request) + estimatedInputTokens = estimateContextWindowInput(request) + } + + if (estimatedInputTokens > inputTokenBudget) { + throw contextWindowError(model, estimatedInputTokens, inputTokenBudget) + } + + return { + systemPrompt: request.systemPrompt, + messages: request.messages, + tools: request.tools, + contextWindow, + outputTokenReserve, + inputTokenBudget, + estimatedInputTokens, + originalEstimatedInputTokens, + wasTruncated: true, + } +} + +/** + * Conservative tokenizer-independent upper bound. UTF-8 byte length is at + * least as large as the byte-level token count used by supported text models; + * explicit structure and vision allowances are added separately. + */ +export function estimateContextWindowInput(request: { + systemPrompt: string + messages: Message[] + tools: ToolDefinition[] +}): number { + let total = REQUEST_OVERHEAD_TOKENS + utf8Length(safeJsonStringify(request.systemPrompt)) + + for (const message of request.messages) { + total += MESSAGE_OVERHEAD_TOKENS + estimateMessage(message) + } + for (const tool of request.tools) { + total += TOOL_OVERHEAD_TOKENS + utf8Length(safeJsonStringify(tool)) + } + + return total +} + +function validateTokenLimit(value: number, label: string): number { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`Invalid model ${label}: ${value}. No LLM call was made.`) + } + return Math.floor(value) +} + +function contextWindowError(model: AxonModel, estimatedInputTokens: number, inputTokenBudget: number): Error { + return new Error( + `Context window guard stopped an unsafe request for ${model.id}: ` + + `${estimatedInputTokens} conservatively estimated input tokens exceed the ${inputTokenBudget}-token input budget ` + + `(${model.contextWindow}-token context window, ${model.maxOutputTokens} tokens reserved for output). ` + + `No LLM call was made.`, + ) +} + +function estimateMessage(message: Message): number { + const record = message as unknown as Record + // Count the complete serialized object so newly introduced/provider-specific + // fields cannot bypass the guard. Vision gets an additional allowance because + // providers tokenize image pixels rather than the JSON URL alone. + return utf8Length(safeJsonStringify(message)) + estimateImageAllowances(record.content) +} + +function estimateImageAllowances(content: unknown): number { + if (!Array.isArray(content)) return 0 + let total = 0 + for (const part of content) { + if (!part || typeof part !== "object") continue + const record = part as Record + if (record.type === "image_url") total += estimateImage(record.image_url) + } + return total +} + +function estimateImage(image: unknown): number { + const url = + typeof image === "string" + ? image + : image && typeof image === "object" && "url" in image + ? String((image as { url?: unknown }).url ?? "") + : "" + if (!url.startsWith("data:")) return REMOTE_IMAGE_TOKENS + const comma = url.indexOf(",") + const encodedLength = comma >= 0 ? url.length - comma - 1 : url.length + const decodedBytes = Math.ceil((encodedLength * 3) / 4) + return Math.max(IMAGE_MINIMUM_TOKENS, decodedBytes) +} + +function cloneMessage(message: Message): MutableMessage { + return structuredClone(message) as MutableMessage +} + +function cloneTool(tool: ToolDefinition): ToolDefinition { + return structuredClone(tool) +} + +function shrinkRequest(request: MutableRequest, ratio: number, round: number): MutableRequest { + const lastIndex = request.messages.length - 1 + return { + systemPrompt: truncateText( + request.systemPrompt, + scaledLength(request.systemPrompt, round < 2 ? Math.max(0.65, ratio) : ratio, round < 3 ? 2_048 : 512), + ), + messages: request.messages.map((message, index) => { + const distanceFromNewest = lastIndex - index + const ageMultiplier = distanceFromNewest > 6 ? 0.35 : distanceFromNewest > 3 ? 0.6 : 1 + return shrinkMessage(message, Math.max(0.01, ratio * ageMultiplier), round) + }), + tools: request.tools.map((tool) => shrinkToolDefinition(tool, ratio)), + } +} + +function shrinkMessage(message: MutableMessage, ratio: number, round: number): MutableMessage { + const copy = { ...message } as MutableMessage + ;(copy as Record).content = shrinkContent(message.content, ratio, round) + + if (message.role === "assistant" && Array.isArray(message.tool_calls)) { + copy.tool_calls = message.tool_calls.map((call) => { + if (call.type !== "function") return call + const argumentsText = call.function.arguments + const limit = scaledLength(argumentsText, ratio, round < 3 ? 128 : 48) + return argumentsText.length <= limit + ? call + : { + ...call, + function: { + ...call.function, + arguments: JSON.stringify({ + _context_window_guard: truncateText(argumentsText, Math.max(16, limit)), + }), + }, + } + }) + } + + return copy +} + +function shrinkContent(content: unknown, ratio: number, round: number): unknown { + if (typeof content === "string") { + return truncateText(content, scaledLength(content, ratio, round < 3 ? 128 : 32)) + } + if (!Array.isArray(content)) return content + + return content.map((part) => { + if (!part || typeof part !== "object") return part + const record = part as Record + if (record.type === "text" && typeof record.text === "string") { + return { + ...record, + text: truncateText(record.text, scaledLength(record.text, ratio, round < 3 ? 128 : 32)), + } + } + return part + }) +} + +function shrinkToolDefinition(tool: ToolDefinition, ratio: number): ToolDefinition { + if (tool.type !== "function") return tool + const description = tool.function.description + return { + ...tool, + function: { + ...tool.function, + ...(description + ? { description: truncateText(description, scaledLength(description, Math.max(0.1, ratio), 48)) } + : {}), + parameters: shrinkSchemaDescriptions( + tool.function.parameters, + ratio, + ) as typeof tool.function.parameters, + }, + } +} + +function shrinkSchemaDescriptions(value: unknown, ratio: number, key?: string): unknown { + if (typeof value === "string") { + return key === "description" || key === "title" + ? truncateText(value, scaledLength(value, Math.max(0.1, ratio), 32)) + : value + } + if (Array.isArray(value)) return value.map((item) => shrinkSchemaDescriptions(item, ratio)) + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([childKey, childValue]) => [ + childKey, + shrinkSchemaDescriptions(childValue, ratio, childKey), + ]), + ) + } + return value +} + +function dropOldestConversationPrefix(messages: MutableMessage[]): MutableMessage[] { + if (messages.length <= 4) return messages + let boundary = Math.max(1, Math.floor(messages.length / 2)) + while (boundary < messages.length && messages[boundary].role !== "user") boundary++ + if (boundary >= messages.length) return messages + + const retained = messages.slice(boundary).map((message) => ({ ...message })) + ;(retained[0] as Record).content = prependTextContent( + retained[0].content, + HISTORY_OMISSION_MARKER, + ) + return retained +} + +function prependTextContent(content: unknown, text: string): unknown { + if (typeof content === "string") return `${text}\n\n${content}` + if (Array.isArray(content)) return [{ type: "text", text }, ...content] + return text +} + +function aggressivelyCompactRequest(request: MutableRequest): MutableRequest { + let messages = request.messages + if (messages.length > 3) messages = dropOldestConversationPrefix(messages) + messages = messages.map((message) => { + const compacted = shrinkMessage(message, 0.003, 10) + if (!Array.isArray(compacted.content)) return compacted + return { + ...compacted, + content: compacted.content.map((part) => { + if (part && typeof part === "object" && "type" in part && part.type === "image_url") { + return { type: "text", text: "[Image omitted by OrbCode's context-window guard.]" } + } + return part + }), + } as MutableMessage + }) + + return { + systemPrompt: truncateText(request.systemPrompt, 1_024), + messages, + tools: [], + } +} + +function scaledLength(text: string, ratio: number, minimum: number): number { + return Math.min(text.length, Math.max(minimum, Math.floor(text.length * ratio))) +} + +export function truncateContextText(text: string, maxCharacters: number): string { + return truncateText(text, maxCharacters) +} + +function truncateText(text: string, maxCharacters: number): string { + if (text.length <= maxCharacters) return text + if (maxCharacters <= TRUNCATION_MARKER.length) return TRUNCATION_MARKER.slice(0, Math.max(0, maxCharacters)) + const available = maxCharacters - TRUNCATION_MARKER.length + const headLength = Math.floor(available * 0.35) + return `${text.slice(0, headLength)}${TRUNCATION_MARKER}${text.slice(-(available - headLength))}` +} + +function utf8Length(value: unknown): number { + return typeof value === "string" ? Buffer.byteLength(value, "utf8") : 0 +} + +function safeJsonStringify(value: unknown): string { + try { + return JSON.stringify(value) ?? String(value) + } catch (error) { + throw new Error( + `Context window guard could not safely estimate an unserializable request value: ${String(error)}. ` + + "No LLM call was made.", + ) + } +} diff --git a/src/api/models.ts b/src/api/models.ts index 3d6848d..52936b4 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -20,6 +20,8 @@ export interface AxonModel { /** USD per token */ outputPrice: number; free: boolean; + /** Human-readable pricing shown when the gateway chooses the billable model dynamically. */ + pricingLabel?: string; /** * Which transport serves this model. Absent (or "matterai"/"axon") routes * through the MatterAI gateway (OpenAI `/chat/completions`). Any other value @@ -141,6 +143,34 @@ export const ANTHROPIC_MODELS: Record = { * interactive picker for now. */ export const BUILTIN_AXON_MODELS: Record = { + "axon-auto-200k": { + id: "axon-auto-200k", + gatewayModelId: "axon-auto", + name: "Axon Auto (200K context)", + description: + "Starts with Code Flash, then dynamically selects Code Flash, Mini, or Pro as the task develops.", + contextWindow: 200000, + maxOutputTokens: 64000, + supportsImages: true, + inputPrice: 0.0000005, + outputPrice: 0.0000015, + free: false, + pricingLabel: "dynamic pricing", + }, + "axon-auto-400k": { + id: "axon-auto-400k", + gatewayModelId: "axon-auto", + name: "Axon Auto (400K context)", + description: + "Starts with Code Flash, then dynamically selects Code Flash, Mini, or Pro as the task develops.", + contextWindow: 400000, + maxOutputTokens: 64000, + supportsImages: true, + inputPrice: 0.0000005, + outputPrice: 0.0000015, + free: false, + pricingLabel: "dynamic pricing", + }, "axon-eido-3-flash": { id: "axon-eido-3-flash", name: "Axon Eido 3 Flash", @@ -271,7 +301,8 @@ export function canUseEidoProModels(plan?: string): boolean { export function is400kAxonModel(modelId: string): boolean { return ( - (modelId.startsWith("axon-eido-3-code-") || + (modelId.startsWith("axon-auto-") || + modelId.startsWith("axon-eido-3-code-") || modelId.startsWith("axon-lumen-4-code-")) && modelId.endsWith("-400k") ); diff --git a/src/api/provider.ts b/src/api/provider.ts index 745f7a9..3bd25c4 100644 --- a/src/api/provider.ts +++ b/src/api/provider.ts @@ -1,5 +1,6 @@ import { AiSdkClient } from "./aiSdkClient.js" import { AxonClient, type AxonClientOptions } from "./client.js" +import { ContextWindowGuardClient } from "./contextWindowGuard.js" import type { LLMClient } from "./llmClient.js" import { getModel, usesAiSdk } from "./models.js" @@ -11,8 +12,6 @@ import { getModel, usesAiSdk } from "./models.js" */ export function createLLMClient(options: AxonClientOptions): LLMClient { const model = getModel(options.modelId) - if (usesAiSdk(model)) { - return new AiSdkClient({ model }) - } - return new AxonClient(options) + const client = usesAiSdk(model) ? new AiSdkClient({ model }) : new AxonClient(options) + return new ContextWindowGuardClient(client, model) } diff --git a/src/ui/components/ModelPicker.tsx b/src/ui/components/ModelPicker.tsx index 661bd26..b72dca6 100644 --- a/src/ui/components/ModelPicker.tsx +++ b/src/ui/components/ModelPicker.tsx @@ -7,8 +7,9 @@ import { PopoverBox } from "./PopoverBox.js" const VISIBLE_ROWS = 6 const CONTEXT_WINDOW_ORDER = [200000, 400000] -// Display order within a context-window group: Flash, Mini, Pro, Lumen. +// Display order within a context-window group: Auto, Flash, Mini, Pro, Lumen. const DISPLAY_ORDER = [ + "axon-auto", "axon-eido-3-flash", "axon-eido-3-code-mini", "axon-eido-3-code-pro", @@ -31,6 +32,7 @@ interface ModelPickerProps { } function formatPrice(model: AxonModel): string { + if (model.pricingLabel) return model.pricingLabel if (model.free) return "free" const perMillion = (price: number) => `$${(price * 1_000_000).toFixed(2)}` return `${perMillion(model.inputPrice)} in / ${perMillion(model.outputPrice)} out per 1M tokens` diff --git a/test/axon-auto-models.test.ts b/test/axon-auto-models.test.ts new file mode 100644 index 0000000..e66057c --- /dev/null +++ b/test/axon-auto-models.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { + BUILTIN_AXON_MODELS, + get200kAxonFallback, + getGatewayModelId, + is400kAxonModel, +} from "../src/api/models.js" + +test("Axon Auto exposes 200K and 400K dynamic-pricing choices", () => { + const auto200k = BUILTIN_AXON_MODELS["axon-auto-200k"] + const auto400k = BUILTIN_AXON_MODELS["axon-auto-400k"] + + assert.equal(auto200k.contextWindow, 200_000) + assert.equal(auto400k.contextWindow, 400_000) + assert.equal(auto200k.pricingLabel, "dynamic pricing") + assert.equal(auto400k.pricingLabel, "dynamic pricing") + assert.equal(auto200k.free, false) + assert.equal(auto400k.free, false) + assert.equal(getGatewayModelId(auto200k), "axon-auto") + assert.equal(getGatewayModelId(auto400k), "axon-auto") +}) + +test("Auto 400K uses the existing extended-context gate and fallback", () => { + assert.equal(is400kAxonModel("axon-auto-400k"), true) + assert.equal(is400kAxonModel("axon-auto-200k"), false) + assert.equal(get200kAxonFallback("axon-auto-400k"), "axon-auto-200k") +}) diff --git a/test/context-window-guard.test.ts b/test/context-window-guard.test.ts new file mode 100644 index 0000000..f598f75 --- /dev/null +++ b/test/context-window-guard.test.ts @@ -0,0 +1,212 @@ +import assert from "node:assert/strict" +import test from "node:test" +import type OpenAI from "openai" + +import { + ContextWindowGuardClient, + estimateContextWindowInput, + prepareContextWindowRequest, +} from "../src/api/contextWindowGuard.js" +import type { LLMClient } from "../src/api/llmClient.js" +import { BUILTIN_AXON_MODELS, type AxonModel } from "../src/api/models.js" +import type { ApiStreamChunk } from "../src/api/stream.js" + +type Message = OpenAI.Chat.ChatCompletionMessageParam +type ToolDefinition = OpenAI.Chat.ChatCompletionTool + +class RecordingClient implements LLMClient { + calls: Array<{ systemPrompt: string; messages: Message[]; tools: ToolDefinition[] }> = [] + + async *createMessage( + systemPrompt: string, + messages: Message[], + tools: ToolDefinition[], + ): AsyncGenerator { + this.calls.push({ systemPrompt, messages, tools }) + yield { type: "text", text: "ok" } + } +} + +function tinyModel(contextWindow: number, maxOutputTokens: number): AxonModel { + return { + id: "tiny-test-model", + name: "Tiny test model", + description: "test", + contextWindow, + maxOutputTokens, + supportsImages: false, + inputPrice: 0, + outputPrice: 0, + free: true, + } +} + +test("derives exact 200k and 400k input budgets from the selected model", () => { + const request = { + systemPrompt: "system", + messages: [{ role: "user", content: "hello" }] satisfies Message[], + tools: [], + } + const model200k = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] + const model400k = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-400k"] + + assert.equal(prepareContextWindowRequest({ model: model200k, ...request }).inputTokenBudget, 116_000) + assert.equal(prepareContextWindowRequest({ model: model400k, ...request }).inputTokenBudget, 296_000) +}) + +test("fits a huge nested tool result before it reaches either provider", () => { + const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] + const messages: Message[] = [ + { role: "user", content: "Read the file" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "read-1", + type: "function", + function: { name: "read_file", arguments: JSON.stringify({ file_path: "huge.txt" }) }, + }, + ], + }, + { role: "tool", tool_call_id: "read-1", content: `BEGIN-${"x".repeat(600_000)}-END` }, + ] + + const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) + + assert.equal(prepared.wasTruncated, true) + assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) + assert.deepEqual(prepared.messages.map((message) => message.role), ["user", "assistant", "tool"]) + assert.match(String(prepared.messages[2].content), /content truncated/) +}) + +test("counts large native and MCP tool schemas in the same request budget", () => { + const messages: Message[] = [{ role: "user", content: "hello" }] + const small = estimateContextWindowInput({ systemPrompt: "system", messages, tools: [] }) + const large = estimateContextWindowInput({ + systemPrompt: "system", + messages, + tools: [ + { + type: "function", + function: { + name: "large_mcp_tool", + description: "schema description ".repeat(20_000), + parameters: { type: "object", properties: {} }, + }, + }, + ], + }) + + assert.ok(large > small + 200_000) +}) + +test("compacts schema prose without changing the tool argument contract", () => { + const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] + const tools: ToolDefinition[] = [ + { + type: "function", + function: { + name: "large_mcp_tool", + description: "large tool description ".repeat(30_000), + parameters: { + type: "object", + properties: { + query: { type: "string", description: "query description ".repeat(30_000) }, + }, + required: ["query"], + }, + }, + }, + ] + + const prepared = prepareContextWindowRequest({ + model, + systemPrompt: "system", + messages: [{ role: "user", content: "search" }], + tools, + }) + const parameters = prepared.tools[0]?.function.parameters as { + properties?: { query?: { type?: string } } + required?: string[] + } + + assert.equal(prepared.wasTruncated, true) + assert.deepEqual(parameters.required, ["query"]) + assert.equal(parameters.properties?.query?.type, "string") + assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) +}) + +test("counts the complete serialized message, including provider-specific fields", () => { + const messages = [ + { + role: "assistant", + content: "done", + _provider_payload: "opaque".repeat(40_000), + } as unknown as Message, + ] + const withoutPayload = estimateContextWindowInput({ + systemPrompt: "system", + messages: [{ role: "assistant", content: "done" }], + tools: [], + }) + const withPayload = estimateContextWindowInput({ systemPrompt: "system", messages, tools: [] }) + + assert.ok(withPayload > withoutPayload + 200_000) +}) + +test("fits oversized initial attachment content without prior usage data", () => { + const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] + const messages: Message[] = [ + { + role: "user", + content: `${"attachment ".repeat(80_000)}`, + }, + ] + + const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) + + assert.equal(prepared.wasTruncated, true) + assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) + assert.match(String(prepared.messages[0].content), /content truncated/) +}) + +test("guards compaction-shaped requests and retains the summary instruction", () => { + const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] + const messages: Message[] = [] + for (let turn = 0; turn < 12; turn++) { + messages.push({ role: "user", content: `old user ${turn} ${"x".repeat(20_000)}` }) + messages.push({ role: "assistant", content: `old assistant ${turn} ${"y".repeat(20_000)}` }) + } + messages.push({ role: "user", content: "Summarize this conversation and respond with only the summary." }) + + const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) + + assert.equal(prepared.wasTruncated, true) + assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) + assert.match(String(prepared.messages.at(-1)?.content), /respond with only the summary/) +}) + +test("the final wrapper never invokes its delegate when no safe request can fit", async () => { + const delegate = new RecordingClient() + const guarded = new ContextWindowGuardClient(delegate, tinyModel(600, 500)) + const stream = guarded.createMessage("system", [{ role: "user", content: "hello" }], []) + + await assert.rejects(stream.next(), /No LLM call was made/) + assert.equal(delegate.calls.length, 0) +}) + +test("the final wrapper passes only fitted content to the transport", async () => { + const delegate = new RecordingClient() + const model = tinyModel(20_000, 2_000) + const guarded = new ContextWindowGuardClient(delegate, model) + const stream = guarded.createMessage("system", [{ role: "user", content: "large input ".repeat(100_000) }], []) + + for await (const _chunk of stream) { + // Drain the generator so the lazy guard and delegate execute. + } + + assert.equal(delegate.calls.length, 1) + const [call] = delegate.calls + assert.ok(estimateContextWindowInput(call) <= 16_000) +}) From 1a575660a1763ab4c5cfc4bcbaa019593b4e8e84 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Mon, 10 Aug 2026 13:19:00 +0530 Subject: [PATCH 2/3] Drop context-window guard from v6.7.6; resync package-lock.json --- CHANGELOG.md | 11 - package-lock.json | 583 ++++++++++++++++++++++++------ package.json | 1 - src/api/contextWindowGuard.ts | 396 -------------------- src/api/provider.ts | 7 +- test/context-window-guard.test.ts | 212 ----------- 6 files changed, 483 insertions(+), 727 deletions(-) delete mode 100644 src/api/contextWindowGuard.ts delete mode 100644 test/context-window-guard.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a1e9a..c8426bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,17 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Fixed - -- Added a final context-window guard around every Axon and AI SDK request, - including normal agent turns, retries, and `/compact`. The guard reserves the - selected model's full output allowance and a safety margin, counts system - prompts, history, tool results, tool arguments, images, and native/MCP tool - schemas, then safely compacts oversized input or stops locally before making - an unsafe provider call. - ## [6.7.4] - 2026-08-04 ### Added diff --git a/package-lock.json b/package-lock.json index 00a2cb5..a91ea25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,28 @@ { "name": "@matterailab/orbcode", - "version": "6.7.4", + "version": "6.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@matterailab/orbcode", - "version": "6.7.4", + "version": "6.7.6", "license": "MIT", "dependencies": { "@ai-sdk/anthropic": "^3.0.85", "@ai-sdk/openai-compatible": "^2.0.51", - "@firecrawl/anydoc": "^0.1.6", "@modelcontextprotocol/sdk": "^1.29.0", "@opentui/core": "0.4.3", "@opentui/react": "0.4.3", "ai": "^6.0.207", "bun": "1.3.14", + "mammoth": "^1.12.0", "open": "^10.1.0", "openai": "^4.78.0", + "pdf-parse": "^2.4.5", "picomatch": "4.0.4", - "react": "^19.2.0" + "react": "^19.2.0", + "read-excel-file": "^9.2.0" }, "bin": { "orbcode": "bin/orbcode.js" @@ -723,31 +725,102 @@ "@ff-labs/fff-bin-win32-x64": "0.10.1" } }, - "node_modules/@firecrawl/anydoc": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc/-/anydoc-0.1.6.tgz", - "integrity": "sha512-zA7xwFn7MHvdb6FOPoRJu3oG0A6sP7zyFIKvp7sGptKfTyU1oqP0yNiAT9oOrRYBvS0gAUHkrfOClplGZXLM7g==", + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", - "bin": { - "anydoc": "cli.js" + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" }, "engines": { - "node": ">= 20" + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" }, "optionalDependencies": { - "@firecrawl/anydoc-darwin-arm64": "0.1.6", - "@firecrawl/anydoc-darwin-x64": "0.1.6", - "@firecrawl/anydoc-linux-arm64-gnu": "0.1.6", - "@firecrawl/anydoc-linux-arm64-musl": "0.1.6", - "@firecrawl/anydoc-linux-x64-gnu": "0.1.6", - "@firecrawl/anydoc-linux-x64-musl": "0.1.6", - "@firecrawl/anydoc-win32-x64-msvc": "0.1.6" + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-darwin-arm64": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-darwin-arm64/-/anydoc-darwin-arm64-0.1.6.tgz", - "integrity": "sha512-2745ZMZ6mPv2ak9j/sz6DDj4+hC9Eg1Lsipya9bHJMm8/WIOtgBF1XW04qKdFEWJQB5VdQ0o1J1tHaEyoUcIpQ==", + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", "cpu": [ "arm64" ], @@ -757,13 +830,13 @@ "darwin" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-darwin-x64": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-darwin-x64/-/anydoc-darwin-x64-0.1.6.tgz", - "integrity": "sha512-kGM0V5mcv/Ut2kVvtWUjH/iPXHdMwUbn4Kyh85lCm6FCgeieD+GYXVREdI3nPUEGMZGdX5i3uEh5sPyIYqGNYA==", + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", "cpu": [ "x64" ], @@ -773,13 +846,29 @@ "darwin" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-linux-arm64-gnu": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-arm64-gnu/-/anydoc-linux-arm64-gnu-0.1.6.tgz", - "integrity": "sha512-ZHA7gNIltemhC6dEMnT2fc49Lo9ucpw6WsxtHBfXgKAZ9beZ5lDMzhZgaFpKSlv+vcnK7cFlOIg5RtRSuZjMlQ==", + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", "cpu": [ "arm64" ], @@ -792,13 +881,13 @@ "linux" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-linux-arm64-musl": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-arm64-musl/-/anydoc-linux-arm64-musl-0.1.6.tgz", - "integrity": "sha512-mnfxDROQ4JWViWOh/58CK+RZBKoUwA4+e02ctUaPqMiBB4tXuxb5s8b5zr3J6ySgvoqe8qJ9UaTcOru1kY0sKg==", + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", "cpu": [ "arm64" ], @@ -811,15 +900,15 @@ "linux" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-linux-x64-gnu": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-x64-gnu/-/anydoc-linux-x64-gnu-0.1.6.tgz", - "integrity": "sha512-bQLhBBeBuNQeRDORwkhUSYjt0+oPYtBt5eLZ5wLf8xFLJmor6e1/yx8CiJBqD93bYbpfqh0r8v6sb+pVsVCgCw==", + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", "cpu": [ - "x64" + "riscv64" ], "libc": [ "glibc" @@ -830,18 +919,18 @@ "linux" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-linux-x64-musl": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-linux-x64-musl/-/anydoc-linux-x64-musl-0.1.6.tgz", - "integrity": "sha512-W/kV3UBBJQ0vPH7ph2UX0XBJq5Qv0L33aZnYw1yFJ5RSSd/PGxSO6xEsyXsOib4SQ7APUq3fp6P5Drj5elfG1Q==", + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", "cpu": [ "x64" ], "libc": [ - "musl" + "glibc" ], "license": "MIT", "optional": true, @@ -849,75 +938,42 @@ "linux" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@firecrawl/anydoc-win32-x64-msvc": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@firecrawl/anydoc-win32-x64-msvc/-/anydoc-win32-x64-msvc-0.1.6.tgz", - "integrity": "sha512-HKcG9YUSb7m9QfwXRaYpgGMkkkByM2dY/KGqFwQJRLtSoMGnQ9pYZSeqRVXsfG+5YXixVTfO4l4Zph54G2RQmg==", + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">= 20" + "node": ">= 10" } }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } + "node": ">= 10" } }, "node_modules/@opentelemetry/api": { @@ -1531,6 +1587,15 @@ "win32" ] }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/@yuuang/ffi-rs-android-arm64": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@yuuang/ffi-rs-android-arm64/-/ffi-rs-android-arm64-1.3.2.tgz", @@ -1845,12 +1910,47 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2042,6 +2142,12 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2164,6 +2270,21 @@ "node": ">=0.3.1" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2459,6 +2580,12 @@ "@yuuang/ffi-rs-win32-x64-msvc": "1.3.2" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2614,6 +2741,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2707,6 +2840,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2785,6 +2924,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2818,6 +2963,62 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/mammoth": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.1.tgz", + "integrity": "sha512-nCH9KKjWi3jQ+i8bUKs7k1yrXtSEGpWgF8IYkzsFMcbn+5S6l4bZEBbyx2hOQErFiXPuAs9RPa6qjXVxhyx/8g==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/marked": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", @@ -2916,6 +3117,12 @@ } } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3021,6 +3228,18 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3030,6 +3249,15 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3049,6 +3277,38 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -3070,6 +3330,12 @@ "node": ">=16.20.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3185,6 +3451,36 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/read-excel-file": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-9.3.9.tgz", + "integrity": "sha512-b4bB8rnZg0g+UI4xR8Ntr84UGKfMOEpT66tX2XMsMloWMCB/jVBsH7EquLG/1c+TuC8+1BuIiFJJghTCLLjZdA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.8.3", + "saxen": "^11.1.0", + "unzipper-esm": "^0.13.3", + "worker-f": "^0.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3222,12 +3518,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/saxen/-/saxen-11.1.1.tgz", + "integrity": "sha512-J4BkmJFaM7VgE7pgkFGsNEcqqM3h7+Mz80vfLWFhx7uNOCOXIu6LLjQHYWNejdst3pf/3JUaBIG9+pkk1umlow==", + "license": "MIT", + "engines": { + "node": ">= 20.12" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -3298,6 +3609,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3410,6 +3727,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3419,6 +3742,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -3554,6 +3886,12 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -3569,6 +3907,25 @@ "node": ">= 0.8" } }, + "node_modules/unzipper-esm": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.3.tgz", + "integrity": "sha512-LUO6VZ6fCzkDbdMev0/fOhoIeVGKaOkTIOoYxVLE0SQjfvmAHK+oywl7lfhloSZIsdGJ25mJ18Mtd9CyTASjrA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3624,6 +3981,15 @@ "node": ">= 8" } }, + "node_modules/worker-f": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/worker-f/-/worker-f-0.1.20.tgz", + "integrity": "sha512-7z5K5z4x++FykhpDTfriT/dOu7CmSap9BBv38DVFU3CD38obopq+DoFH75yBV3butpGm+OeqR9BKdSvYJSnUfQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3667,6 +4033,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 335bedc..585e7b3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "dev": "bun src/index.tsx", "start": "bun dist/index.js", "test:attachments": "node --import tsx --test test/attachments.test.ts", - "test:context": "node --import tsx --test test/context-window-guard.test.ts", "test:mcp": "node --import tsx --test test/mcp-client.test.ts", "test:plugins": "node --import tsx --test test/plugins.test.ts", "test:metrics": "node --import tsx --test test/metrics.test.ts", diff --git a/src/api/contextWindowGuard.ts b/src/api/contextWindowGuard.ts deleted file mode 100644 index d2eb72b..0000000 --- a/src/api/contextWindowGuard.ts +++ /dev/null @@ -1,396 +0,0 @@ -import type OpenAI from "openai" - -import type { LLMClient } from "./llmClient.js" -import type { AxonModel } from "./models.js" -import type { ApiStreamChunk } from "./stream.js" - -export const CONTEXT_WINDOW_SAFETY_PERCENTAGE = 0.1 - -const REQUEST_OVERHEAD_TOKENS = 512 -const MESSAGE_OVERHEAD_TOKENS = 32 -const TOOL_OVERHEAD_TOKENS = 32 -const IMAGE_MINIMUM_TOKENS = 16_384 -const REMOTE_IMAGE_TOKENS = 64_000 -const TRUNCATION_MARKER = "\n[...content truncated to fit the selected model's context window...]\n" -const HISTORY_OMISSION_MARKER = - "[Earlier conversation turns were omitted by OrbCode's context-window guard before this request.]" - -type Message = OpenAI.Chat.ChatCompletionMessageParam -type ToolDefinition = OpenAI.Chat.ChatCompletionTool -type MutableMessage = Message & Record - -export interface PreparedContextWindowRequest { - systemPrompt: string - messages: Message[] - tools: ToolDefinition[] - contextWindow: number - outputTokenReserve: number - inputTokenBudget: number - estimatedInputTokens: number - originalEstimatedInputTokens: number - wasTruncated: boolean -} - -interface MutableRequest { - systemPrompt: string - messages: MutableMessage[] - tools: ToolDefinition[] -} - -/** Install one final request boundary around either OrbCode transport. */ -export class ContextWindowGuardClient implements LLMClient { - constructor( - private readonly delegate: LLMClient, - private readonly model: AxonModel, - ) {} - - async *createMessage( - systemPrompt: string, - messages: Message[], - tools: ToolDefinition[], - abortSignal?: AbortSignal, - ): AsyncGenerator { - const prepared = prepareContextWindowRequest({ - model: this.model, - systemPrompt, - messages, - tools, - }) - - yield* this.delegate.createMessage( - prepared.systemPrompt, - prepared.messages, - prepared.tools, - abortSignal, - ) - } -} - -export function prepareContextWindowRequest({ - model, - systemPrompt, - messages, - tools, -}: { - model: AxonModel - systemPrompt: string - messages: Message[] - tools: ToolDefinition[] -}): PreparedContextWindowRequest { - const contextWindow = validateTokenLimit(model.contextWindow, "context window") - const outputTokenReserve = validateTokenLimit(model.maxOutputTokens, "maximum output") - const inputTokenBudget = Math.floor( - contextWindow * (1 - CONTEXT_WINDOW_SAFETY_PERCENTAGE) - outputTokenReserve, - ) - - if (inputTokenBudget <= REQUEST_OVERHEAD_TOKENS) { - throw contextWindowError(model, REQUEST_OVERHEAD_TOKENS, inputTokenBudget) - } - - let request: MutableRequest = { - systemPrompt, - messages: messages.map(cloneMessage), - tools: tools.map(cloneTool), - } - const originalEstimatedInputTokens = estimateContextWindowInput(request) - let estimatedInputTokens = originalEstimatedInputTokens - - if (estimatedInputTokens <= inputTokenBudget) { - return { - systemPrompt: request.systemPrompt, - messages: request.messages, - tools: request.tools, - contextWindow, - outputTokenReserve, - inputTokenBudget, - estimatedInputTokens, - originalEstimatedInputTokens, - wasTruncated: false, - } - } - - for (let round = 0; round < 6 && estimatedInputTokens > inputTokenBudget; round++) { - const ratio = Math.max(0.01, Math.min(0.85, (inputTokenBudget / estimatedInputTokens) * 0.82)) - request = shrinkRequest(request, ratio, round) - estimatedInputTokens = estimateContextWindowInput(request) - } - - while (estimatedInputTokens > inputTokenBudget && request.messages.length > 4) { - const shortened = dropOldestConversationPrefix(request.messages) - if (shortened.length === request.messages.length) break - request.messages = shortened - estimatedInputTokens = estimateContextWindowInput(request) - } - - if (estimatedInputTokens > inputTokenBudget) { - request = aggressivelyCompactRequest(request) - estimatedInputTokens = estimateContextWindowInput(request) - } - - if (estimatedInputTokens > inputTokenBudget) { - throw contextWindowError(model, estimatedInputTokens, inputTokenBudget) - } - - return { - systemPrompt: request.systemPrompt, - messages: request.messages, - tools: request.tools, - contextWindow, - outputTokenReserve, - inputTokenBudget, - estimatedInputTokens, - originalEstimatedInputTokens, - wasTruncated: true, - } -} - -/** - * Conservative tokenizer-independent upper bound. UTF-8 byte length is at - * least as large as the byte-level token count used by supported text models; - * explicit structure and vision allowances are added separately. - */ -export function estimateContextWindowInput(request: { - systemPrompt: string - messages: Message[] - tools: ToolDefinition[] -}): number { - let total = REQUEST_OVERHEAD_TOKENS + utf8Length(safeJsonStringify(request.systemPrompt)) - - for (const message of request.messages) { - total += MESSAGE_OVERHEAD_TOKENS + estimateMessage(message) - } - for (const tool of request.tools) { - total += TOOL_OVERHEAD_TOKENS + utf8Length(safeJsonStringify(tool)) - } - - return total -} - -function validateTokenLimit(value: number, label: string): number { - if (!Number.isFinite(value) || value <= 0) { - throw new Error(`Invalid model ${label}: ${value}. No LLM call was made.`) - } - return Math.floor(value) -} - -function contextWindowError(model: AxonModel, estimatedInputTokens: number, inputTokenBudget: number): Error { - return new Error( - `Context window guard stopped an unsafe request for ${model.id}: ` + - `${estimatedInputTokens} conservatively estimated input tokens exceed the ${inputTokenBudget}-token input budget ` + - `(${model.contextWindow}-token context window, ${model.maxOutputTokens} tokens reserved for output). ` + - `No LLM call was made.`, - ) -} - -function estimateMessage(message: Message): number { - const record = message as unknown as Record - // Count the complete serialized object so newly introduced/provider-specific - // fields cannot bypass the guard. Vision gets an additional allowance because - // providers tokenize image pixels rather than the JSON URL alone. - return utf8Length(safeJsonStringify(message)) + estimateImageAllowances(record.content) -} - -function estimateImageAllowances(content: unknown): number { - if (!Array.isArray(content)) return 0 - let total = 0 - for (const part of content) { - if (!part || typeof part !== "object") continue - const record = part as Record - if (record.type === "image_url") total += estimateImage(record.image_url) - } - return total -} - -function estimateImage(image: unknown): number { - const url = - typeof image === "string" - ? image - : image && typeof image === "object" && "url" in image - ? String((image as { url?: unknown }).url ?? "") - : "" - if (!url.startsWith("data:")) return REMOTE_IMAGE_TOKENS - const comma = url.indexOf(",") - const encodedLength = comma >= 0 ? url.length - comma - 1 : url.length - const decodedBytes = Math.ceil((encodedLength * 3) / 4) - return Math.max(IMAGE_MINIMUM_TOKENS, decodedBytes) -} - -function cloneMessage(message: Message): MutableMessage { - return structuredClone(message) as MutableMessage -} - -function cloneTool(tool: ToolDefinition): ToolDefinition { - return structuredClone(tool) -} - -function shrinkRequest(request: MutableRequest, ratio: number, round: number): MutableRequest { - const lastIndex = request.messages.length - 1 - return { - systemPrompt: truncateText( - request.systemPrompt, - scaledLength(request.systemPrompt, round < 2 ? Math.max(0.65, ratio) : ratio, round < 3 ? 2_048 : 512), - ), - messages: request.messages.map((message, index) => { - const distanceFromNewest = lastIndex - index - const ageMultiplier = distanceFromNewest > 6 ? 0.35 : distanceFromNewest > 3 ? 0.6 : 1 - return shrinkMessage(message, Math.max(0.01, ratio * ageMultiplier), round) - }), - tools: request.tools.map((tool) => shrinkToolDefinition(tool, ratio)), - } -} - -function shrinkMessage(message: MutableMessage, ratio: number, round: number): MutableMessage { - const copy = { ...message } as MutableMessage - ;(copy as Record).content = shrinkContent(message.content, ratio, round) - - if (message.role === "assistant" && Array.isArray(message.tool_calls)) { - copy.tool_calls = message.tool_calls.map((call) => { - if (call.type !== "function") return call - const argumentsText = call.function.arguments - const limit = scaledLength(argumentsText, ratio, round < 3 ? 128 : 48) - return argumentsText.length <= limit - ? call - : { - ...call, - function: { - ...call.function, - arguments: JSON.stringify({ - _context_window_guard: truncateText(argumentsText, Math.max(16, limit)), - }), - }, - } - }) - } - - return copy -} - -function shrinkContent(content: unknown, ratio: number, round: number): unknown { - if (typeof content === "string") { - return truncateText(content, scaledLength(content, ratio, round < 3 ? 128 : 32)) - } - if (!Array.isArray(content)) return content - - return content.map((part) => { - if (!part || typeof part !== "object") return part - const record = part as Record - if (record.type === "text" && typeof record.text === "string") { - return { - ...record, - text: truncateText(record.text, scaledLength(record.text, ratio, round < 3 ? 128 : 32)), - } - } - return part - }) -} - -function shrinkToolDefinition(tool: ToolDefinition, ratio: number): ToolDefinition { - if (tool.type !== "function") return tool - const description = tool.function.description - return { - ...tool, - function: { - ...tool.function, - ...(description - ? { description: truncateText(description, scaledLength(description, Math.max(0.1, ratio), 48)) } - : {}), - parameters: shrinkSchemaDescriptions( - tool.function.parameters, - ratio, - ) as typeof tool.function.parameters, - }, - } -} - -function shrinkSchemaDescriptions(value: unknown, ratio: number, key?: string): unknown { - if (typeof value === "string") { - return key === "description" || key === "title" - ? truncateText(value, scaledLength(value, Math.max(0.1, ratio), 32)) - : value - } - if (Array.isArray(value)) return value.map((item) => shrinkSchemaDescriptions(item, ratio)) - if (value && typeof value === "object") { - return Object.fromEntries( - Object.entries(value).map(([childKey, childValue]) => [ - childKey, - shrinkSchemaDescriptions(childValue, ratio, childKey), - ]), - ) - } - return value -} - -function dropOldestConversationPrefix(messages: MutableMessage[]): MutableMessage[] { - if (messages.length <= 4) return messages - let boundary = Math.max(1, Math.floor(messages.length / 2)) - while (boundary < messages.length && messages[boundary].role !== "user") boundary++ - if (boundary >= messages.length) return messages - - const retained = messages.slice(boundary).map((message) => ({ ...message })) - ;(retained[0] as Record).content = prependTextContent( - retained[0].content, - HISTORY_OMISSION_MARKER, - ) - return retained -} - -function prependTextContent(content: unknown, text: string): unknown { - if (typeof content === "string") return `${text}\n\n${content}` - if (Array.isArray(content)) return [{ type: "text", text }, ...content] - return text -} - -function aggressivelyCompactRequest(request: MutableRequest): MutableRequest { - let messages = request.messages - if (messages.length > 3) messages = dropOldestConversationPrefix(messages) - messages = messages.map((message) => { - const compacted = shrinkMessage(message, 0.003, 10) - if (!Array.isArray(compacted.content)) return compacted - return { - ...compacted, - content: compacted.content.map((part) => { - if (part && typeof part === "object" && "type" in part && part.type === "image_url") { - return { type: "text", text: "[Image omitted by OrbCode's context-window guard.]" } - } - return part - }), - } as MutableMessage - }) - - return { - systemPrompt: truncateText(request.systemPrompt, 1_024), - messages, - tools: [], - } -} - -function scaledLength(text: string, ratio: number, minimum: number): number { - return Math.min(text.length, Math.max(minimum, Math.floor(text.length * ratio))) -} - -export function truncateContextText(text: string, maxCharacters: number): string { - return truncateText(text, maxCharacters) -} - -function truncateText(text: string, maxCharacters: number): string { - if (text.length <= maxCharacters) return text - if (maxCharacters <= TRUNCATION_MARKER.length) return TRUNCATION_MARKER.slice(0, Math.max(0, maxCharacters)) - const available = maxCharacters - TRUNCATION_MARKER.length - const headLength = Math.floor(available * 0.35) - return `${text.slice(0, headLength)}${TRUNCATION_MARKER}${text.slice(-(available - headLength))}` -} - -function utf8Length(value: unknown): number { - return typeof value === "string" ? Buffer.byteLength(value, "utf8") : 0 -} - -function safeJsonStringify(value: unknown): string { - try { - return JSON.stringify(value) ?? String(value) - } catch (error) { - throw new Error( - `Context window guard could not safely estimate an unserializable request value: ${String(error)}. ` + - "No LLM call was made.", - ) - } -} diff --git a/src/api/provider.ts b/src/api/provider.ts index 3bd25c4..745f7a9 100644 --- a/src/api/provider.ts +++ b/src/api/provider.ts @@ -1,6 +1,5 @@ import { AiSdkClient } from "./aiSdkClient.js" import { AxonClient, type AxonClientOptions } from "./client.js" -import { ContextWindowGuardClient } from "./contextWindowGuard.js" import type { LLMClient } from "./llmClient.js" import { getModel, usesAiSdk } from "./models.js" @@ -12,6 +11,8 @@ import { getModel, usesAiSdk } from "./models.js" */ export function createLLMClient(options: AxonClientOptions): LLMClient { const model = getModel(options.modelId) - const client = usesAiSdk(model) ? new AiSdkClient({ model }) : new AxonClient(options) - return new ContextWindowGuardClient(client, model) + if (usesAiSdk(model)) { + return new AiSdkClient({ model }) + } + return new AxonClient(options) } diff --git a/test/context-window-guard.test.ts b/test/context-window-guard.test.ts deleted file mode 100644 index f598f75..0000000 --- a/test/context-window-guard.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -import assert from "node:assert/strict" -import test from "node:test" -import type OpenAI from "openai" - -import { - ContextWindowGuardClient, - estimateContextWindowInput, - prepareContextWindowRequest, -} from "../src/api/contextWindowGuard.js" -import type { LLMClient } from "../src/api/llmClient.js" -import { BUILTIN_AXON_MODELS, type AxonModel } from "../src/api/models.js" -import type { ApiStreamChunk } from "../src/api/stream.js" - -type Message = OpenAI.Chat.ChatCompletionMessageParam -type ToolDefinition = OpenAI.Chat.ChatCompletionTool - -class RecordingClient implements LLMClient { - calls: Array<{ systemPrompt: string; messages: Message[]; tools: ToolDefinition[] }> = [] - - async *createMessage( - systemPrompt: string, - messages: Message[], - tools: ToolDefinition[], - ): AsyncGenerator { - this.calls.push({ systemPrompt, messages, tools }) - yield { type: "text", text: "ok" } - } -} - -function tinyModel(contextWindow: number, maxOutputTokens: number): AxonModel { - return { - id: "tiny-test-model", - name: "Tiny test model", - description: "test", - contextWindow, - maxOutputTokens, - supportsImages: false, - inputPrice: 0, - outputPrice: 0, - free: true, - } -} - -test("derives exact 200k and 400k input budgets from the selected model", () => { - const request = { - systemPrompt: "system", - messages: [{ role: "user", content: "hello" }] satisfies Message[], - tools: [], - } - const model200k = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] - const model400k = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-400k"] - - assert.equal(prepareContextWindowRequest({ model: model200k, ...request }).inputTokenBudget, 116_000) - assert.equal(prepareContextWindowRequest({ model: model400k, ...request }).inputTokenBudget, 296_000) -}) - -test("fits a huge nested tool result before it reaches either provider", () => { - const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] - const messages: Message[] = [ - { role: "user", content: "Read the file" }, - { - role: "assistant", - content: null, - tool_calls: [ - { - id: "read-1", - type: "function", - function: { name: "read_file", arguments: JSON.stringify({ file_path: "huge.txt" }) }, - }, - ], - }, - { role: "tool", tool_call_id: "read-1", content: `BEGIN-${"x".repeat(600_000)}-END` }, - ] - - const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) - - assert.equal(prepared.wasTruncated, true) - assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) - assert.deepEqual(prepared.messages.map((message) => message.role), ["user", "assistant", "tool"]) - assert.match(String(prepared.messages[2].content), /content truncated/) -}) - -test("counts large native and MCP tool schemas in the same request budget", () => { - const messages: Message[] = [{ role: "user", content: "hello" }] - const small = estimateContextWindowInput({ systemPrompt: "system", messages, tools: [] }) - const large = estimateContextWindowInput({ - systemPrompt: "system", - messages, - tools: [ - { - type: "function", - function: { - name: "large_mcp_tool", - description: "schema description ".repeat(20_000), - parameters: { type: "object", properties: {} }, - }, - }, - ], - }) - - assert.ok(large > small + 200_000) -}) - -test("compacts schema prose without changing the tool argument contract", () => { - const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] - const tools: ToolDefinition[] = [ - { - type: "function", - function: { - name: "large_mcp_tool", - description: "large tool description ".repeat(30_000), - parameters: { - type: "object", - properties: { - query: { type: "string", description: "query description ".repeat(30_000) }, - }, - required: ["query"], - }, - }, - }, - ] - - const prepared = prepareContextWindowRequest({ - model, - systemPrompt: "system", - messages: [{ role: "user", content: "search" }], - tools, - }) - const parameters = prepared.tools[0]?.function.parameters as { - properties?: { query?: { type?: string } } - required?: string[] - } - - assert.equal(prepared.wasTruncated, true) - assert.deepEqual(parameters.required, ["query"]) - assert.equal(parameters.properties?.query?.type, "string") - assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) -}) - -test("counts the complete serialized message, including provider-specific fields", () => { - const messages = [ - { - role: "assistant", - content: "done", - _provider_payload: "opaque".repeat(40_000), - } as unknown as Message, - ] - const withoutPayload = estimateContextWindowInput({ - systemPrompt: "system", - messages: [{ role: "assistant", content: "done" }], - tools: [], - }) - const withPayload = estimateContextWindowInput({ systemPrompt: "system", messages, tools: [] }) - - assert.ok(withPayload > withoutPayload + 200_000) -}) - -test("fits oversized initial attachment content without prior usage data", () => { - const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] - const messages: Message[] = [ - { - role: "user", - content: `${"attachment ".repeat(80_000)}`, - }, - ] - - const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) - - assert.equal(prepared.wasTruncated, true) - assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) - assert.match(String(prepared.messages[0].content), /content truncated/) -}) - -test("guards compaction-shaped requests and retains the summary instruction", () => { - const model = BUILTIN_AXON_MODELS["axon-eido-3-code-mini-200k"] - const messages: Message[] = [] - for (let turn = 0; turn < 12; turn++) { - messages.push({ role: "user", content: `old user ${turn} ${"x".repeat(20_000)}` }) - messages.push({ role: "assistant", content: `old assistant ${turn} ${"y".repeat(20_000)}` }) - } - messages.push({ role: "user", content: "Summarize this conversation and respond with only the summary." }) - - const prepared = prepareContextWindowRequest({ model, systemPrompt: "system", messages, tools: [] }) - - assert.equal(prepared.wasTruncated, true) - assert.ok(prepared.estimatedInputTokens <= prepared.inputTokenBudget) - assert.match(String(prepared.messages.at(-1)?.content), /respond with only the summary/) -}) - -test("the final wrapper never invokes its delegate when no safe request can fit", async () => { - const delegate = new RecordingClient() - const guarded = new ContextWindowGuardClient(delegate, tinyModel(600, 500)) - const stream = guarded.createMessage("system", [{ role: "user", content: "hello" }], []) - - await assert.rejects(stream.next(), /No LLM call was made/) - assert.equal(delegate.calls.length, 0) -}) - -test("the final wrapper passes only fitted content to the transport", async () => { - const delegate = new RecordingClient() - const model = tinyModel(20_000, 2_000) - const guarded = new ContextWindowGuardClient(delegate, model) - const stream = guarded.createMessage("system", [{ role: "user", content: "large input ".repeat(100_000) }], []) - - for await (const _chunk of stream) { - // Drain the generator so the lazy guard and delegate execute. - } - - assert.equal(delegate.calls.length, 1) - const [call] = delegate.calls - assert.ok(estimateContextWindowInput(call) <= 16_000) -}) From 768cb4e6f11c5c94dacbd20252e664d946991010 Mon Sep 17 00:00:00 2001 From: code-crusher Date: Mon, 10 Aug 2026 13:25:30 +0530 Subject: [PATCH 3/3] Add axon-eido-3-flash-400k model option --- CHANGELOG.md | 10 ++++++++++ src/api/models.ts | 17 +++++++++++++++++ test/axon-auto-models.test.ts | 13 +++++++++++++ 3 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8426bc..d1e969e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Axon Eido 3 Flash 400K context option.** Added `axon-eido-3-flash-400k`, + a 400K context variant of the flash model that maps to the same + `axon-eido-3-flash` gateway model. Gated to Pro Plus and Ultra plans like + the other 400K options; the picker renders it under the 400K group and the + 200K fallback resolves to the bare `axon-eido-3-flash` id. + ## [6.7.4] - 2026-08-04 ### Added diff --git a/src/api/models.ts b/src/api/models.ts index 52936b4..2baacbd 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -183,6 +183,19 @@ export const BUILTIN_AXON_MODELS: Record = { outputPrice: 0.0000015, free: false, }, + "axon-eido-3-flash-400k": { + id: "axon-eido-3-flash-400k", + gatewayModelId: "axon-eido-3-flash", + name: "Axon Eido 3 Flash (400K context)", + description: + "Axon Eido is a fast and low cost general purpose model for low-effort day-to-day tasks", + contextWindow: 400000, + maxOutputTokens: 64000, + supportsImages: true, + inputPrice: 0.0000005, + outputPrice: 0.0000015, + free: false, + }, "axon-eido-3-code-pro-200k": { id: "axon-eido-3-code-pro-200k", gatewayModelId: "axon-eido-3-code-pro", @@ -303,6 +316,7 @@ export function is400kAxonModel(modelId: string): boolean { return ( (modelId.startsWith("axon-auto-") || modelId.startsWith("axon-eido-3-code-") || + modelId.startsWith("axon-eido-3-flash-") || modelId.startsWith("axon-lumen-4-code-")) && modelId.endsWith("-400k") ); @@ -317,6 +331,9 @@ export function isEidoProAxonModel(modelId: string): boolean { } export function get200kAxonFallback(modelId: string): string { + // The 200K Flash option uses the bare id (no "-200k" suffix), so the + // generic -400k → -200k rewrite would point at a non-existent model. + if (modelId === "axon-eido-3-flash-400k") return "axon-eido-3-flash"; return modelId.replace(/-400k$/, "-200k"); } diff --git a/test/axon-auto-models.test.ts b/test/axon-auto-models.test.ts index e66057c..b85475c 100644 --- a/test/axon-auto-models.test.ts +++ b/test/axon-auto-models.test.ts @@ -27,3 +27,16 @@ test("Auto 400K uses the existing extended-context gate and fallback", () => { assert.equal(is400kAxonModel("axon-auto-200k"), false) assert.equal(get200kAxonFallback("axon-auto-400k"), "axon-auto-200k") }) + +test("Axon Eido 3 Flash exposes a 400K variant through the same gates", () => { + const flash400k = BUILTIN_AXON_MODELS["axon-eido-3-flash-400k"] + + assert.equal(flash400k.contextWindow, 400_000) + assert.equal(flash400k.maxOutputTokens, 64_000) + assert.equal(getGatewayModelId(flash400k), "axon-eido-3-flash") + assert.equal(is400kAxonModel("axon-eido-3-flash-400k"), true) + assert.equal(is400kAxonModel("axon-eido-3-flash"), false) + // The 200K Flash option uses the bare id (no "-200k" suffix), so the + // fallback must map to that exact id rather than the generic -200k form. + assert.equal(get200kAxonFallback("axon-eido-3-flash-400k"), "axon-eido-3-flash") +})