diff --git a/.gitattributes b/.gitattributes index de987de7b9..27c4c4e6de 100644 --- a/.gitattributes +++ b/.gitattributes @@ -25,3 +25,9 @@ src/opensquilla/squilla_router/models/v4.2_phase3_inference/*.yaml text eol=lf src/opensquilla/squilla_router/models/v4.2_phase3_inference/**/*.json text eol=lf src/opensquilla/squilla_router/models/v4.2_phase3_inference/**/*.txt text eol=lf src/opensquilla/squilla_router/models/v4.2_phase3_inference/**/*.yaml text eol=lf + +# Frozen LightGBM model-policy inference assets. +src/opensquilla/squilla_router/lightgbm_model/** text eol=lf +src/opensquilla/squilla_router/lightgbm_model/assets/**/*.joblib filter=lfs diff=lfs merge=lfs -text +src/opensquilla/squilla_router/lightgbm_model/assets/**/*.pkl filter=lfs diff=lfs merge=lfs -text +src/opensquilla/squilla_router/lightgbm_model/assets/**/*.gz filter=lfs diff=lfs merge=lfs -text diff --git a/.github/actions/prepare-router-assets/action.yml b/.github/actions/prepare-router-assets/action.yml new file mode 100644 index 0000000000..7f2d4f52fe --- /dev/null +++ b/.github/actions/prepare-router-assets/action.yml @@ -0,0 +1,16 @@ +name: Prepare router assets +description: Prepare verified model assets and native dependencies before Gateway startup or packaging. +runs: + using: composite + steps: + - name: Hydrate and verify router assets + shell: bash + working-directory: ${{ github.workspace }} + run: | + set -euo pipefail + git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" + PYTHONPATH=src python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + - name: Install macOS router runtime library + if: ${{ runner.os == 'macOS' }} + shell: bash + run: brew install libomp diff --git a/.github/ci/suites.v1.json b/.github/ci/suites.v1.json index 72e5d625c2..b405447322 100644 --- a/.github/ci/suites.v1.json +++ b/.github/ci/suites.v1.json @@ -56,7 +56,8 @@ "tests/test_provider_ensemble.py", "tests/test_scripts/test_verify_webui_artifact.py", "tests/test_scripts/test_stage_webui_artifact.py", - "tests/test_packaging/test_webui_build_contract.py" + "tests/test_packaging/test_webui_build_contract.py", + "tests/test_lightgbm_router_model.py" ], "baseline_suites": [ "dependency-audit", @@ -543,7 +544,9 @@ "tests/test_provider_ensemble.py", "tests/test_scripts/test_verify_webui_artifact.py", "tests/test_scripts/test_stage_webui_artifact.py", - "tests/test_packaging/test_webui_build_contract.py" + "tests/test_packaging/test_webui_build_contract.py", + "src/opensquilla/squilla_router/lightgbm_model/**", + "tests/test_lightgbm_router_model.py" ] }, "release-packaging": { diff --git a/.github/ci/trust-policy.v1.json b/.github/ci/trust-policy.v1.json index 5a04b50cf7..c56c886edc 100644 --- a/.github/ci/trust-policy.v1.json +++ b/.github/ci/trust-policy.v1.json @@ -2,6 +2,7 @@ "schema_version": 1, "merge_critical_inputs": [ ".github/actions/download-required-artifact/action.yml", + ".github/actions/prepare-router-assets/action.yml", ".github/ci/dependency-audit.v1.json", ".github/ci/suites.v1.json", ".github/ci/trust-policy.v1.json", diff --git a/.github/scripts/wait_ci_candidate.py b/.github/scripts/wait_ci_candidate.py index 54b12d9eb4..44788a07aa 100644 --- a/.github/scripts/wait_ci_candidate.py +++ b/.github/scripts/wait_ci_candidate.py @@ -29,6 +29,8 @@ def api_failure( ) -> tuple[str, bool]: if isinstance(error, subprocess.TimeoutExpired): return "command-timeout", True + if error.returncode != 1: + return f"cli-exit-{error.returncode}", False stderr = error.stderr if isinstance(error.stderr, str) else "" # HTTP failures take precedence even if their response mentions a timeout. status = re.search(r"\bHTTP (\d{3})\b", stderr) @@ -44,7 +46,13 @@ def api_failure( r"\bClient\.Timeout exceeded while awaiting headers\b|" r"\bnet/http: timeout awaiting response headers\b", stderr, re.IGNORECASE): return "network-timeout", True - return "unknown-cli-failure", False + if re.search(r"\bx509\b|certificate verify failed", stderr, re.IGNORECASE): + return "certificate-error", False + # gh uses exit 1 for general API failures, including transport errors whose + # wording varies across platforms. These are read-only metadata requests: + # bound unknown failures by the same retry/deadline limits as named errors. + # Other exits (including cancellation and authentication) remain terminal. + return "unknown-cli-failure", error.returncode == 1 def remaining_wait(deadline: float) -> float: @@ -77,7 +85,7 @@ def api(endpoint: str, *, pages: bool = False, deadline: float | None = None) -> except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: reason, retryable = api_failure(exc) # This helper only reads Actions metadata. Never echo its captured - # response/body or retry authentication, validation or unknown errors. + # response/body or retry known authentication/validation failures. if attempt == 2 or not retryable: raise MetadataApiError( f"GitHub Actions metadata request failed: {reason}; " diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55614b3192..b08199c826 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -908,6 +908,9 @@ jobs: - name: Install Gateway dependencies run: uv sync --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Validate renderer capacity inventory id: capacity-inventory # Pure WebUI plans do not select Python shards. Run the existing @@ -1069,6 +1072,9 @@ jobs: - name: Install Gateway dependencies run: uv sync --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Validate renderer capacity inventory id: capacity-inventory # Pure WebUI plans do not select Python shards. Run the existing @@ -1371,6 +1377,9 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Lint shell: bash run: uv run ruff check src tests @@ -1389,11 +1398,17 @@ jobs: [[ "$TIMING_BASE" =~ ^[0-9a-f]{40}$ ]] || { echo "Expected a full baseline commit SHA" >&2; exit 2; } git fetch --no-tags --depth=1 origin "$TIMING_BASE" baseline_dir="$(mktemp -d "$(dirname "$GITHUB_WORKSPACE")/gateway-timing-baseline.XXXXXX")" - trap 'rm -rf "$baseline_dir"' EXIT - git archive "$TIMING_BASE" > "$RUNNER_TEMP/gateway-timing-baseline.tar" - git get-tar-commit-id < "$RUNNER_TEMP/gateway-timing-baseline.tar" > "$baseline_dir/.gateway-timing-source-sha" - tar -xf "$RUNNER_TEMP/gateway-timing-baseline.tar" -C "$baseline_dir" + git worktree add --detach "$baseline_dir" "$TIMING_BASE" + trap 'git worktree remove --force "$baseline_dir"' EXIT + git -C "$baseline_dir" lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" + if [[ -f "$baseline_dir/src/opensquilla/squilla_router/lightgbm_model/model.py" ]]; then + PYTHONPATH="$baseline_dir/src" python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + fi + comparison_python="$(uv run --no-sync python -c 'import sys; print(sys._base_executable)')" + UV_PROJECT_ENVIRONMENT="$baseline_dir/.venv" uv sync --project "$baseline_dir" \ + --python "$comparison_python" --extra dev --extra recommended --extra mcp --frozen uv run --no-sync python scripts/compare_gateway_timing.py \ + --baseline-python "$baseline_dir/.venv/bin/python" \ --baseline-root "$baseline_dir" --candidate-root "$GITHUB_WORKSPACE" \ --baseline-sha "$TIMING_BASE" --candidate-sha "$(git rev-parse HEAD)" \ --samples 3 --output "$RUNNER_TEMP/gateway-timing.json" @@ -1511,6 +1526,9 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Preflight offline test environment shell: bash run: | @@ -1745,6 +1763,9 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Record tool versions shell: bash run: | @@ -2017,6 +2038,9 @@ jobs: - name: Install dependencies shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets - name: Test bundle partition shell: bash run: | @@ -2149,10 +2173,14 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Preflight offline test environment shell: bash run: | set -euo pipefail + uv run python -c "import platform; print('router test architecture:', platform.machine()); assert platform.machine().lower() in {'arm64', 'aarch64'}, 'Router parity must run on Apple Silicon'" uv run python -c "import pathlib, subprocess, sys, opensquilla; print('cwd:', pathlib.Path.cwd().resolve()); print('git:', subprocess.check_output(['git', 'rev-parse', '--show-toplevel', 'HEAD'], text=True).strip()); print('python:', sys.executable); print('package:', pathlib.Path(opensquilla.__file__).resolve())" uv run pytest \ tests/test_sandbox/test_trusted_sandbox_execution.py \ @@ -2167,6 +2195,7 @@ jobs: run: | set -euo pipefail pytest_args=( + tests/test_lightgbm_router_model.py tests/test_recovery tests/test_migration/test_opensquilla_home_migration.py tests/test_desktop/test_electron_startup_contract.py @@ -2359,6 +2388,9 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Install Desktop dependencies working-directory: desktop/electron shell: bash @@ -3055,6 +3087,9 @@ jobs: - name: Install test dependencies run: uv sync --extra dev --extra recommended --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Run offline Community Skill contracts shell: bash env: @@ -3146,6 +3181,9 @@ jobs: shell: bash run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Run release packaging contract tests shell: bash run: | @@ -3241,6 +3279,9 @@ jobs: - name: Install locked development dependencies run: uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Build production WebUI artifact if: ${{ github.event_name == 'push' }} working-directory: opensquilla-webui diff --git a/.github/workflows/desktop-native-probes.yml b/.github/workflows/desktop-native-probes.yml index 9da165af27..55f6e00e81 100644 --- a/.github/workflows/desktop-native-probes.yml +++ b/.github/workflows/desktop-native-probes.yml @@ -53,6 +53,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/desktop-native" printf 'CI_REPORT_DIR=%s\n' "${RUNNER_TEMP}/desktop-native" >> "${GITHUB_ENV}" uv sync --extra dev --extra recommended --extra mcp --frozen + - name: Prepare router assets for Gateway probes + if: ${{ matrix.probe != 'pty' }} + uses: ./.github/actions/prepare-router-assets - name: Verify recommended PTY and managed execution on the native platform if: ${{ matrix.probe == 'pty' }} shell: bash @@ -112,7 +115,6 @@ jobs: shell: bash run: | set -euo pipefail - git lfs pull --include="src/opensquilla/squilla_router/models/v4.2_phase3_inference/**" mcp_image="opensquilla-mcp-ci:${GITHUB_SHA}" docker build --file Dockerfile --tag "${mcp_image}" . \ 2>&1 | tee "${CI_REPORT_DIR}/mcp-docker-build.log" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5e4542043d..c00fc9857a 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -81,7 +81,13 @@ jobs: fi - name: Hydrate router assets - run: git lfs pull --include="src/opensquilla/squilla_router/models/**" + run: git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" + + - name: Verify LightGBM router assets + run: | + if [[ -f src/opensquilla/squilla_router/lightgbm_model/model.py ]]; then + PYTHONPATH=src python3 -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + fi # The Dockerfile hard-fails on LFS pointer files; failing here first # gives a clearer error than a mid-build SystemExit. diff --git a/.github/workflows/gateway-authorization-timing.yml b/.github/workflows/gateway-authorization-timing.yml index 69c214bc3f..bb4ff3b212 100644 --- a/.github/workflows/gateway-authorization-timing.yml +++ b/.github/workflows/gateway-authorization-timing.yml @@ -14,7 +14,7 @@ name: Gateway authorization timing workflow_dispatch: inputs: baseline_sha: - description: Full baseline commit SHA with identical dependency inputs + description: Full baseline commit SHA (uses its own frozen dependency environment) required: true type: string @@ -47,8 +47,11 @@ jobs: with: version: "0.12.15" enable-cache: true - - name: Install identical comparison dependencies + - name: Install candidate comparison dependencies run: uv sync --extra dev --extra recommended --extra mcp --frozen + + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets - name: Compare offline Gateway startup and Default first turn shell: bash env: @@ -75,16 +78,32 @@ jobs: output = Path(os.environ["RUNNER_TEMP"]) / "gateway-timing" / "timing.json" subprocess.run(["git", "worktree", "add", "--detach", str(baseline), baseline_sha], check=True) try: + subprocess.run([ + "git", "-C", str(baseline), "lfs", "pull", + "--include=src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**", + ], check=True) + if (baseline / "src/opensquilla/squilla_router/lightgbm_model/model.py").is_file(): + subprocess.run([ + sys.executable, "-c", + "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())", + ], cwd=baseline, env={**os.environ, "PYTHONPATH": str(baseline / "src")}, check=True) + subprocess.run([ + "uv", "sync", "--project", str(baseline), + "--python", sys._base_executable, + "--extra", "dev", "--extra", "recommended", "--extra", "mcp", "--frozen", + ], env={**os.environ, "UV_PROJECT_ENVIRONMENT": str(baseline / ".venv")}, check=True) + baseline_python = baseline / ".venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python") subprocess.run([ sys.executable, "scripts/compare_gateway_timing.py", "--baseline-root", str(baseline), "--candidate-root", str(candidate), "--baseline-sha", baseline_sha, "--candidate-sha", candidate_sha, + "--baseline-python", str(baseline_python), "--samples", "3", "--output", str(output), ], check=True) report = json.loads(output.read_text(encoding="utf-8")) with Path(os.environ["GITHUB_STEP_SUMMARY"]).open("a", encoding="utf-8") as summary: summary.write("## Offline Gateway timing — " + os.environ["TIMING_PLATFORM"] + "\n\n") - summary.write("Three alternating samples per source; offline provider, not model TTFT.\n\n") + summary.write("Three alternating samples per source; same Python version, each source uses its own frozen dependencies. Includes dependency changes; offline provider, not model TTFT.\n\n") summary.write("| Metric | Baseline median (ms) | Candidate median (ms) | Ratio |\n") summary.write("| --- | ---: | ---: | ---: |\n") for metric, ratio in report["candidate_over_baseline"].items(): diff --git a/.github/workflows/live-release-e2e.yml b/.github/workflows/live-release-e2e.yml index c8af7e76bc..65023f7ad8 100644 --- a/.github/workflows/live-release-e2e.yml +++ b/.github/workflows/live-release-e2e.yml @@ -52,6 +52,9 @@ jobs: - name: Install dependencies run: uv sync --extra dev --extra recommended --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Fail if OpenRouter secret is missing shell: bash run: | diff --git a/.github/workflows/webui-browser-smoke.yml b/.github/workflows/webui-browser-smoke.yml index fb4f1d9eb2..7df378cd84 100644 --- a/.github/workflows/webui-browser-smoke.yml +++ b/.github/workflows/webui-browser-smoke.yml @@ -50,6 +50,9 @@ jobs: - name: Install Python dependencies run: uv sync --extra dev --frozen + - name: Prepare router assets + uses: ./.github/actions/prepare-router-assets + - name: Install Playwright browser run: npx --yes playwright install chromium diff --git a/.github/workflows/wheelhouse-release.yml b/.github/workflows/wheelhouse-release.yml index 41fd55c47c..a9a74df077 100644 --- a/.github/workflows/wheelhouse-release.yml +++ b/.github/workflows/wheelhouse-release.yml @@ -263,7 +263,7 @@ jobs: - name: Hydrate router assets shell: bash - run: git lfs pull --include="src/opensquilla/squilla_router/models/**" + run: git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" - name: Verify router assets are hydrated shell: bash @@ -294,6 +294,13 @@ jobs: with: python-version: "3.12" + - name: Verify LightGBM router assets + shell: bash + run: | + if [[ -f src/opensquilla/squilla_router/lightgbm_model/model.py ]]; then + PYTHONPATH=src python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + fi + - name: Set up Node.js for Web UI staging if: needs.build-control-ui.outputs.webui_mode == 'source-built' uses: actions/setup-node@v4 @@ -472,13 +479,23 @@ jobs: path: ${{ needs.build-control-ui.outputs.webui_mode == 'source-built' && 'opensquilla-webui/dist/' || 'src/opensquilla/gateway/static/dist/' }} - name: Hydrate router assets - run: git lfs pull --include="src/opensquilla/squilla_router/models/**" + run: git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Verify LightGBM router assets + shell: bash + run: | + if [[ -f src/opensquilla/squilla_router/lightgbm_model/model.py ]]; then + PYTHONPATH=src python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + fi + + - name: Install macOS router runtime library + run: brew install libomp + - name: Set up uv uses: astral-sh/setup-uv@v5 with: @@ -649,13 +666,20 @@ jobs: - name: Hydrate router assets shell: bash - run: git lfs pull --include="src/opensquilla/squilla_router/models/**" + run: git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**" - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" + - name: Verify LightGBM router assets + shell: bash + run: | + if [[ -f src/opensquilla/squilla_router/lightgbm_model/model.py ]]; then + PYTHONPATH=src python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + fi + - name: Set up uv uses: astral-sh/setup-uv@v5 with: diff --git a/.github/workflows/windows-nsis-upgrade-regression.yml b/.github/workflows/windows-nsis-upgrade-regression.yml index 59e6801114..f1cc7ff946 100644 --- a/.github/workflows/windows-nsis-upgrade-regression.yml +++ b/.github/workflows/windows-nsis-upgrade-regression.yml @@ -141,8 +141,11 @@ jobs: enable-cache: true - name: Hydrate router assets run: | - git lfs pull --include='src/opensquilla/squilla_router/models/**' + git lfs pull --include='src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**' if ($LASTEXITCODE -ne 0) { throw 'Router LFS fetch failed' } + $env:PYTHONPATH = 'src' + python -c "from opensquilla.squilla_router.lightgbm_model import validate_artifacts; print(validate_artifacts())" + if ($LASTEXITCODE -ne 0) { throw 'Router artifact validation failed' } - name: Build and verify complete WebUI if: ${{ (inputs.frontend_artifact_name || '') == '' }} working-directory: opensquilla-webui diff --git a/desktop/electron/scripts/build-gateway.mjs b/desktop/electron/scripts/build-gateway.mjs index 1c346cbbf3..f5ea047e26 100644 --- a/desktop/electron/scripts/build-gateway.mjs +++ b/desktop/electron/scripts/build-gateway.mjs @@ -23,6 +23,7 @@ const caRuntimeHookPath = join(scriptDir, 'pyinstaller_runtime_hooks', 'ensure_c const controlUiDistDir = join(repoRoot, 'opensquilla-webui', 'dist') const controlUiVerifier = join(repoRoot, 'opensquilla-webui', 'scripts', 'verify-dist.mjs') const routerBundleDir = join(repoRoot, 'src', 'opensquilla', 'squilla_router', 'models', 'v4.2_phase3_inference') +const lightgbmModelDir = join(repoRoot, 'src', 'opensquilla', 'squilla_router', 'lightgbm_model', 'assets') // The V4 router ships its implementation as a copied runtime_src tree and // imports it only after validating the bundle. PyInstaller cannot resolve // that delayed `src.router.*` import from the normal repository search path; @@ -320,6 +321,7 @@ function recordStage(stage, started) { assertControlUiArtifactReady() assertRouterAssetsReady() assertRouterIntegrity(routerBundleDir) +assertRouterIntegrity(lightgbmModelDir) const buildInputs = gatewayInputs(repoRoot) rmSync(runtimeGatewayDir, { recursive: true, force: true }) @@ -458,6 +460,8 @@ const args = [ '--add-data', `${join(tokenizerCacheDir, tokenizerCacheKey)}${addDataSeparator}tiktoken-cache`, '--add-data', + `${lightgbmModelDir}${addDataSeparator}opensquilla/squilla_router/lightgbm_model/assets`, + '--add-data', `${join(repoRoot, 'migrations')}${addDataSeparator}opensquilla/_migrations`, '--add-data', `${migrationRegistry}${addDataSeparator}opensquilla/_migrations`, diff --git a/desktop/electron/scripts/gateway-integrity.mjs b/desktop/electron/scripts/gateway-integrity.mjs index bc05c6a490..82c2c4abc7 100644 --- a/desktop/electron/scripts/gateway-integrity.mjs +++ b/desktop/electron/scripts/gateway-integrity.mjs @@ -4,6 +4,7 @@ import { join } from 'node:path' export const gatewayBuildRecord = 'gateway-build.json' const routerRelative = 'squilla_router/models/v4.2_phase3_inference' +const lightgbmRelative = 'squilla_router/lightgbm_model/assets' const lfsHeader = 'version https://git-lfs.github.com/spec/v1' const rebuild = 'Run npm run build:web && npm run build:gateway before prepared packaging.' @@ -104,6 +105,7 @@ export function assertGatewayResources(repoRoot, runtimeRoot) { ])) assertInventory(actualRegistry, expectedRegistry, 'frozen migration registry') assertRouterIntegrity(join(packageDir, routerRelative), join(repoRoot, 'src', 'opensquilla', routerRelative)) + assertRouterIntegrity(join(packageDir, lightgbmRelative), join(repoRoot, 'src', 'opensquilla', lightgbmRelative)) } export function gatewayInputs(repoRoot) { diff --git a/desktop/electron/scripts/test-desktop-account-quit.mjs b/desktop/electron/scripts/test-desktop-account-quit.mjs index 5ab00f3558..f815bac0e9 100644 --- a/desktop/electron/scripts/test-desktop-account-quit.mjs +++ b/desktop/electron/scripts/test-desktop-account-quit.mjs @@ -149,6 +149,41 @@ function failedGateway(h, { hangs = false } = {}) { } } +for (const platform of ['win32', 'darwin', 'linux']) { + test(`account bridge stays reachable while Gateway drains on ${platform}`, async () => { + const h = harness() + const gatewayDrain = deferred() + const child = { pid: 12345 } + let exited = false + h.env.process.platform = platform + h.env.gatewayProcessTreeTerminations = new Map() + h.env.gatewayProcess = child + h.env.gatewayState.owned = true + h.env.hasGatewayProcessExited = () => exited + h.env.liveLifecycleOwnedGatewayProcesses = () => exited ? [] : [child] + h.env.quitOwnedGateway = async () => { + h.events.push('gateway-close') + await gatewayDrain.promise + exited = true + return true + } + h.accountDrain.resolve() + h.quit() + try { + await new Promise(resolve => setImmediate(resolve)) + assert.ok(h.events.includes('gateway-close')) + assert.ok(h.events.includes('account-close'), 'account writes drain immediately') + assert.equal(h.events.includes('proxy-close'), false, 'Gateway may still read account state') + } finally { + gatewayDrain.resolve() + await h.env.quitGatewayDrainPromise + } + assert.ok(h.events.includes('proxy-close')) + assert.ok(h.events.includes('exit:0')) + assert.deepEqual(h.errors, []) + }) +} + test('ordinary Quit cannot force exit while account persistence is unfinished', async () => { const h = harness({ budgetMs: 30 }) failedGateway(h) diff --git a/desktop/electron/scripts/test-desktop-router-config.mjs b/desktop/electron/scripts/test-desktop-router-config.mjs index d968b140d1..1010abc37d 100644 --- a/desktop/electron/scripts/test-desktop-router-config.mjs +++ b/desktop/electron/scripts/test-desktop-router-config.mjs @@ -23,15 +23,15 @@ function toPlain(value) { const defaults = { c0: { provider: 'tokenrhythm', model: 'qwen3.7-flash' }, - c1: { provider: 'tokenrhythm', model: 'deepseek-flash' }, + c1: { provider: 'tokenrhythm', model: 'deepseek-v4-flash-0731' }, c2: { provider: 'tokenrhythm', model: 'deepseek-v4-pro-0813' }, - c3: { provider: 'tokenrhythm', model: 'glm-5.3', ensembleEnabled: false }, + c3: { provider: 'tokenrhythm', model: 'kimi-k3', ensembleEnabled: false }, } assert.equal(defaultRouterTiers('openrouter', 'recommended').c1.provider, 'openrouter') assert.equal(defaultRouterTiers('tokenrhythm', 'recommended').c3.ensembleEnabled, false) const textTiers = ['c0', 'c1', 'c2', 'c3'] for (const [provider, models] of Object.entries({ - openrouter: ['qwen/qwen3.7-flash', 'deepseek/deepseek-v4-flash-0731', 'deepseek/deepseek-v4-pro-0813', 'z-ai/glm-5.3'], + openrouter: ['qwen/qwen3.7-flash', 'deepseek/deepseek-v4-flash', 'deepseek/deepseek-v4-pro', 'moonshotai/kimi-k3'], tokenrhythm: Object.values(defaults).map(tier => tier.model), })) { const profile = defaultRouterTiers(provider, 'recommended') @@ -59,7 +59,7 @@ assert.equal(update({}, savedFusion).routerTiers.c3.ensembleEnabled, true) assert.equal(update({}, savedFusion).routerTiers.c3.model, 'glm-5.2') const resetFusion = update({ routerResetToRecommended: true }, savedFusion) assert.equal(resetFusion.routerTiers.c3.ensembleEnabled, false) -assert.equal(resetFusion.routerTiers.c3.model, 'glm-5.3') +assert.equal(resetFusion.routerTiers.c3.model, 'kimi-k3') for (const invalid of [undefined, null, '', 'recommended', 'FOLLOW_PRIMARY', {}, true]) { assert.equal(normalizeRouterPresetBinding(invalid), undefined) diff --git a/desktop/electron/scripts/test-gateway-integrity.mjs b/desktop/electron/scripts/test-gateway-integrity.mjs index 0ce3698235..e2bff30b6e 100644 --- a/desktop/electron/scripts/test-gateway-integrity.mjs +++ b/desktop/electron/scripts/test-gateway-integrity.mjs @@ -20,6 +20,16 @@ function fixture(t) { schema_version: 1, files: [{ path: 'model.onnx', size_bytes: 5, sha256: fileHash(join(repo, router, 'model.onnx')) }], })) + const lightgbm = 'src/opensquilla/squilla_router/lightgbm_model/assets' + for (const path of ['intent/model.txt.gz', 'tier/model.txt.gz', 'preprocess/model.onnx']) { + write(`${lightgbm}/${path}`, 'model') + } + write(`${lightgbm}/artifact_manifest.json`, JSON.stringify({ + schema_version: 1, + files: ['intent/model.txt.gz', 'tier/model.txt.gz', 'preprocess/model.onnx'].map(path => ({ + path, size_bytes: 5, sha256: fileHash(join(repo, lightgbm, path)), + })), + })) write('src/opensquilla/gateway/boot.py', 'current gateway') // Same numeric prefix is legal: compare full IDs, not max version or count. write('migrations/V010__one.py', 'first migration') @@ -49,6 +59,7 @@ function fixture(t) { }])), })) cpSync(join(repo, router), join(packageDir, 'squilla_router/models/v4.2_phase3_inference'), { recursive: true }) + cpSync(join(repo, lightgbm), join(packageDir, 'squilla_router/lightgbm_model/assets'), { recursive: true }) write('runtime/opensquilla-gateway/opensquilla-gateway.exe', 'unsigned gateway') write('runtime/dependency-inventory.json', JSON.stringify({ schemaVersion: 1, kind: 'pyinstaller', lockSha256: fileHash(join(repo, 'uv.lock')), @@ -158,3 +169,14 @@ test('old provenance cannot be reused for a modified executable', (t) => { write('runtime/opensquilla-gateway/opensquilla-gateway.exe', 'stale executable') assert.throws(() => verifyGatewayIntegrity(repo, runtime, { prepared: true }), /prepared Gateway outputs/) }) + +for (const asset of ['intent/model.txt.gz', 'tier/model.txt.gz', 'preprocess/model.onnx']) { + test(`LightGBM ${asset} missing or corrupt is rejected`, (t) => { + const { repo, runtime, packageDir } = fixture(t) + const path = join(packageDir, 'squilla_router/lightgbm_model/assets', asset) + writeFileSync(path, 'other') + assert.throws(() => assertGatewayResources(repo, runtime), /Router/) + rmSync(path) + assert.throws(() => assertGatewayResources(repo, runtime), /Router/) + }) +} diff --git a/desktop/electron/scripts/test-onboarding-flow.mjs b/desktop/electron/scripts/test-onboarding-flow.mjs index 604369e09f..bcc9412f1e 100644 --- a/desktop/electron/scripts/test-onboarding-flow.mjs +++ b/desktop/electron/scripts/test-onboarding-flow.mjs @@ -10,6 +10,10 @@ import { _electron as electron } from 'playwright' import { desktopRouterConfigTomlLines } from '../dist/desktop-router-config.js' import { DESKTOP_GATEWAY_STARTUP_TIMEOUT_MS } from '../dist/gateway-lifecycle.js' import { parse, stringify } from 'smol-toml' +import { + closeElectronWithDeadline, + requestElectronQuitWithConfirmation, +} from './e2e-shutdown-helpers.mjs' import { LocalAccountSecrets } from '../dist/local-account-secrets.js' import { renderUnconfiguredDesktopConfig } from '../dist/desktop-unconfigured-profile.js' @@ -445,6 +449,15 @@ async function assertSubmitActionsDoNotOverlap(page) { } } +async function pauseClockAtCurrentTime(page) { + const now = Date.now() + // Freeze Date across the pause RPC so host scheduling cannot make its target + // stale, then let Date advance with the manually controlled timers again. + await page.clock.setFixedTime(now) + await page.clock.pauseAt(now) + await page.clock.setSystemTime(now) +} + async function verifyBootPhaseTimer(app) { const page = await bootWindow(app) const phase = page.locator('#phase') @@ -473,9 +486,7 @@ async function verifyBootPhaseTimer(app) { // The boot window sits behind the setup invitation, where Chromium may throttle // its 100 ms timer. Drive elapsed time explicitly instead of racing a brief // wall-clock reset window, and install before reload so every timer is owned. - const bootClockOrigin = Date.now() - await page.clock.install({ time: bootClockOrigin }) - await page.clock.pauseAt(bootClockOrigin + 1_000) + await pauseClockAtCurrentTime(page) await page.reload({ waitUntil: 'domcontentloaded' }) await waitForBootProgress(page, persistedProgress) await waitFor(async () => ( @@ -859,9 +870,7 @@ async function verifySubmitFeedbackAndSingleFlight() { // gateway-owned shell lifecycle. await loadBootContractWindow(app) await verifyBootPhaseTimer(app) - const submitClockOrigin = Date.now() - await page.clock.install({ time: submitClockOrigin }) - await page.clock.pauseAt(submitClockOrigin + 1_000) + await pauseClockAtCurrentTime(page) const apiKey = page.locator('#apiKey') await apiKey.fill('synthetic-submit-key') await page.locator('#onboardingLocale').selectOption('de') @@ -1330,6 +1339,7 @@ const observeRenderer = (candidate) => { } for (const candidate of app.windows()) observeRenderer(candidate) app.on('window', observeRenderer) +let providerClientFailed = false try { const page = await setupWindow(app) @@ -1426,7 +1436,7 @@ try { assert.equal(await page.locator('#modelSummary').isVisible(), true) assert.equal(await page.locator('#modelEditor').isVisible(), false) assert.equal(await page.locator('#modelSummaryLabel').innerText(), '推荐模型') - assert.equal(await page.locator('#modelSummaryValue').innerText(), 'deepseek-flash') + assert.equal(await page.locator('#modelSummaryValue').innerText(), 'deepseek-v4-flash-0731') assert.deepEqual( await page.evaluate(() => [ getComputedStyle(document.getElementById('providerSelectLabel')).fontSize, @@ -1536,7 +1546,7 @@ try { assert.equal(await page.locator('#provider').inputValue(), 'tokenrhythm') assert.equal(await page.locator('#baseUrl').inputValue(), 'https://tokenrhythm.studio/v1') - assert.equal(await page.locator('#model').inputValue(), 'deepseek-flash') + assert.equal(await page.locator('#model').inputValue(), 'deepseek-v4-flash-0731') assert.equal(await page.locator('#modelRoutingMode').inputValue(), 'squilla_router') assert.equal(await page.locator('#routerMode').inputValue(), 'recommended') @@ -1601,7 +1611,7 @@ try { assert.equal(await page.locator('#modelRoutingMode').inputValue(), 'squilla_router') assert.equal(await page.locator('#routerMode').inputValue(), 'recommended') assert.equal(await page.locator('#modelSummary').isVisible(), true) - assert.equal(await page.locator('#modelSummaryValue').innerText(), 'deepseek-flash') + assert.equal(await page.locator('#modelSummaryValue').innerText(), 'deepseek-v4-flash-0731') await page.locator('#apiKey').fill('synthetic-tokenrhythm-key') assert.equal(await page.locator('.inline-search-section').isVisible(), true) assert.equal(await page.locator('#inlineSearchHeading').innerText(), 'Choose web search') @@ -1708,11 +1718,11 @@ try { const remainingGrowthSpool = await readDirectoryOrEmpty(join(earlySpoolRoot, 'growth')) assert.equal(remainingGrowthSpool.some(isManagedTelemetrySpoolEntry), false) assert.equal(credential.routerDefaultTier, 'c1') - assert.equal(credential.model, 'deepseek-flash') + assert.equal(credential.model, 'deepseek-v4-flash-0731') assert.equal(credential.routerTiers.c0.model, 'qwen3.7-flash') - assert.equal(credential.routerTiers.c1.model, 'deepseek-flash') + assert.equal(credential.routerTiers.c1.model, 'deepseek-v4-flash-0731') assert.equal(credential.routerTiers.c2.model, 'deepseek-v4-pro-0813') - assert.equal(credential.routerTiers.c3.model, 'glm-5.3') + assert.equal(credential.routerTiers.c3.model, 'kimi-k3') assert.equal(Object.hasOwn(credential.routerTiers.c0, 'supportsImage'), false) assert.equal(Object.hasOwn(credential.routerTiers.c1, 'supportsImage'), false) assert.equal(Object.hasOwn(credential.routerTiers.c2, 'supportsImage'), false) @@ -1721,23 +1731,23 @@ try { assert.equal(credential.routerTiers.image_model.model, 'kimi-k2.6') assert.equal(Object.hasOwn(credential.routerTiers.image_model, 'supportsImage'), false) assert.match(config, /\[squilla_router\]\nenabled = true/) - assert.match(config, /\[llm\][\s\S]*?model = "deepseek-flash"/) + assert.match(config, /\[llm\][\s\S]*?model = "deepseek-v4-flash-0731"/) assert.match(config, /\[squilla_router\.tiers\.c0\]\nprovider = "tokenrhythm"\nmodel = "qwen3.7-flash"/) - assert.match(config, /\[squilla_router\.tiers\.c1\]\nprovider = "tokenrhythm"\nmodel = "deepseek-flash"/) + assert.match(config, /\[squilla_router\.tiers\.c1\]\nprovider = "tokenrhythm"\nmodel = "deepseek-v4-flash-0731"/) assert.match(config, /\[squilla_router\.tiers\.c2\]\nprovider = "tokenrhythm"\nmodel = "deepseek-v4-pro-0813"/) - assert.match(config, /\[squilla_router\.tiers\.c3\][\s\S]*?model = "glm-5.3"[\s\S]*?ensemble_enabled = false/) + assert.match(config, /\[squilla_router\.tiers\.c3\][\s\S]*?model = "kimi-k3"[\s\S]*?ensemble_enabled = false/) assert.doesNotMatch(config, /thinking_level\s*=/) assert.doesNotMatch(config, /supports_image\s*=/) assert.match(config, /\[llm_ensemble\]\nenabled = false/) assert.equal(successfulProbeServer.requests.length, 0, 'saving complete, untested configuration must not make a provider request') - const readyConnection = await waitFor(async () => { - const connection = await desktopPage.evaluate( - () => window.opensquillaDesktop?.getGatewayConnection?.(), - ) - return connection?.status === 'ready' ? connection : null - }, 'ready Desktop Gateway descriptor') + assert.equal(await readyDesktopWindow(app), desktopPage, + 'Gateway readiness must preserve the original local Desktop renderer') + const readyConnection = await desktopPage.evaluate( + () => window.opensquillaDesktop?.getGatewayConnection?.(), + ) + assert.equal(readyConnection?.status, 'ready') assert.match(readyConnection.httpUrl, /^http:\/\/127\.0\.0\.1:\d+$/) assert.equal( readyConnection.wsUrl, @@ -1878,7 +1888,7 @@ try { const reset = await saveDesktop({ routerResetToRecommended: true, routerTiers: { c1: { provider: 'tokenrhythm', model: 'untrusted-renderer-model' } } }) assert.equal(reset.routerPresetBinding, 'follow_primary') - assert.equal(reset.routerTiers.c1.model, 'deepseek-flash') + assert.equal(reset.routerTiers.c1.model, 'deepseek-v4-flash-0731') assert.match(await readFile(routerConfigPath, 'utf8'), /preset_binding = "follow_primary"/) // Switching a generated Desktop profile must follow config.toml ownership, @@ -1925,11 +1935,29 @@ try { screenshotPath: screenshotPath || null, }, null, 2)) } catch (error) { + providerClientFailed = true await reportOnboardingFailure(app, 'provider-client', error) throw error } finally { - await app.close().catch(() => {}) + const shutdown = await closeElectronWithDeadline({ + app, + phase: 'onboarding-provider-client', + requestClose: options => requestElectronQuitWithConfirmation(app, { + ...options, requireConfirmation: false, + }), + diagnostics: async () => { + const readLog = async name => redactDiagnosticText(( + await readFile(join(userDataDir, 'logs', name), 'utf8').catch(() => '') + ).slice(-24_000)) + const [desktopLogTail, gatewayLogTail] = await Promise.all([ + readLog('desktop.log'), readLog('gateway.log'), + ]) + return { desktopLogTail, gatewayLogTail } + }, + }) await successfulProbeServer.close().catch(() => {}) await rm(userDataRoot, { recursive: true, force: true }).catch(() => {}) + // The helper reports cleanup failures; keep an earlier assertion primary. + if (!shutdown.closed && !providerClientFailed) throw shutdown.error } console.log('PASS onboarding provider-client') diff --git a/desktop/electron/src/generated/desktop-router-catalog.ts b/desktop/electron/src/generated/desktop-router-catalog.ts index 8c61ff21be..7c48af048d 100644 --- a/desktop/electron/src/generated/desktop-router-catalog.ts +++ b/desktop/electron/src/generated/desktop-router-catalog.ts @@ -11,8 +11,8 @@ export const ROUTER_PROFILES: Record> = { }, "c1": { "provider": "tokenrhythm", - "model": "deepseek-flash", - "description": "Default DeepSeek Flash route for normal agent work, coding assistance, debugging, and moderate analysis" + "model": "deepseek-v4-flash-0731", + "description": "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis" }, "c2": { "provider": "tokenrhythm", @@ -21,8 +21,8 @@ export const ROUTER_PROFILES: Record> = { }, "c3": { "provider": "tokenrhythm", - "model": "glm-5.3", - "description": "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", + "model": "kimi-k3", + "description": "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", "ensembleEnabled": false }, "image_model": { @@ -41,20 +41,20 @@ export const ROUTER_PROFILES: Record> = { }, "c1": { "provider": "openrouter", - "model": "deepseek/deepseek-v4-flash-0731", - "description": "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis", + "model": "deepseek/deepseek-v4-flash", + "description": "Default DeepSeek V4 Flash route for normal agent work, coding assistance, debugging, and moderate analysis", "thinkingLevel": "high" }, "c2": { "provider": "openrouter", - "model": "deepseek/deepseek-v4-pro-0813", - "description": "Stronger DeepSeek V4 Pro 0813 route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis", + "model": "deepseek/deepseek-v4-pro", + "description": "Stronger DeepSeek V4 Pro route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis", "thinkingLevel": "high" }, "c3": { "provider": "openrouter", - "model": "z-ai/glm-5.3", - "description": "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", + "model": "moonshotai/kimi-k3", + "description": "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", "thinkingLevel": "high" }, "image_model": { diff --git a/desktop/electron/src/main.ts b/desktop/electron/src/main.ts index f8d94d729f..c73edc25e7 100644 --- a/desktop/electron/src/main.ts +++ b/desktop/electron/src/main.ts @@ -17157,7 +17157,15 @@ app.on('before-quit', (event) => { : budget.termination(Promise.allSettled( accountEntries.flatMap(entry => [ Promise.resolve().then(() => entry.account.close()), - Promise.resolve().then(() => entry.proxy.close()), + Promise.resolve().then(async () => { + // Gateway status reads still use the loopback bridge while it + // drains. Closing the listener first can stall Windows connects + // and prevent the Gateway from releasing its profile ownership. + // Account admission/persistence closes immediately above; only + // the bridge listener survives within the existing quit budget. + await budget.cleanup(childCleanup) + await entry.proxy.close() + }), ]), )).then(result => { const accountsStopped = result.completed && result.value.every(item => item.status === 'fulfilled') diff --git a/docs/features/squilla-router.md b/docs/features/squilla-router.md index 9d350f4465..c2d94f8749 100644 --- a/docs/features/squilla-router.md +++ b/docs/features/squilla-router.md @@ -21,8 +21,11 @@ It is not required. OpenSquilla can also run in direct single-model mode. ## Base Model and Memory -Routing loads the packaged base model, or the explicit `squilla_router.v4_bundle_dir` -path. Router self-learning and its feedback submission API have been removed. On upgrade, +Routing defaults to the packaged intent and tier LightGBM classifiers with +`strategy = "lightgbm_model"` and `tier_policy = "model"`. An explicit +`squilla_router.lightgbm_model_dir` must contain the same verified model bundle. +`squilla_router.v4_bundle_dir` applies only to the explicit `legacy_v4_phase3` +strategy. Router self-learning and its feedback submission API have been removed. On upgrade, the obsolete `squilla_router.self_learning` settings are discarded automatically; credentials, model tiers, explicit model paths, and Dream settings are preserved. A failed automatic config backup or rewrite does not prevent loading the migrated @@ -30,8 +33,9 @@ settings in memory. Explicit save failures still report an error. Historical learned-model pointers, samples, and training directories are left in place and are no longer read. Model choices may therefore differ from a previously -personalized model. Routing diagnostics, manual model selection, calibration, and -immediate complaint escalation remain available. +personalized model. Routing diagnostics and manual model selection remain available. +The model policy handles a classified correction or explicit Redo as a one-tier +advance within the current task. Chat thumbs up/down rate the answer independently of routing. Selections are kept only for the current page and reset on reload; they are not submitted or used for @@ -41,6 +45,58 @@ Memory Dream runs independently. Saving its enabled state or schedule reconciles its scheduled jobs; if that cannot take effect live, settings report that a restart is required. Dream's preview and memory-writing behavior are unchanged. +## Upgrading to the Model Policy + +Saved `strategy = "v4_phase3"` configurations now load as `lightgbm_model`. +An omitted strategy also selects the new default. An explicitly disabled router +stays disabled. Check custom ladders and budget expectations before enabling the +new path; the following behavior changes apply even to an existing V4 configuration: + +- A new task uses the tier classifier. Continue keeps the last admitted tier; + Redo advances it by one, capped at C3. Explicit New Task starts a new context + generation. The two classifiers and their preprocessing run locally. + When that generation exceeds the request window, the existing summarizer first + condenses its own history under the configured budget. The summary is request-local: + it neither reads an older task's checkpoint nor rewrites the full transcript. + With summarization disabled or unavailable, the temporary window still reports + omitted history; repeated long requests may need to summarize again. +- Confidence, probability-margin, difficulty, continuity, and initial budget-cap + heuristics do not rewrite the selected tier. In particular, the existing router + budget configuration does not demote or cap the initial model-policy choice. + A quality retry still checks the current budget before advancing. +- Each tier executes one configured model. The model policy does not enter C3 + Ensemble or silently select another tier/provider when credentials, image + support, or request capacity fail admission. The turn fails with that admission + error. Choose compatible models for every tier that can receive image requests. + Tool-produced image continuations also keep the admitted tier without rerouting; + a deployment without proven vision support is explicitly rejected. +- Reasoning capability is required only when the effective request enables + thinking. Explicit `llm.thinking = "off"` remains authoritative, including + with `squilla_router.auto_thinking = false`. +- A validation failure or repeated tool calls without progress can request one + quality upgrade. The complete target request must pass admission before its + tier is committed. Quality and transport retries share a maximum of three + additional provider calls, further bounded by `max_provider_retries`. + Supported stream-to-non-stream recovery and Codex credential refresh remain + available before response output: the coordinator reserves the same allowance + before retrying, with at most one credential refresh per request. Child agents + have an independent allowance with the same configured bound; they do not inherit + the parent's routing state or quality-upgrade callback. +- Missing or invalid model assets fail closed. Source checkouts must hydrate + Git LFS model weights before starting the Gateway. Packaged builds validate + the same frozen asset hashes. + +To retain the prior routing behavior while reviewing this migration, explicitly set: + +```toml +[squilla_router] +strategy = "legacy_v4_phase3" +``` + +To turn off routing altogether, use `enabled = false` in that section. Direct and +explicit Ensemble modes retain their own configuration; the single-model rule +above describes the new model-policy router. + ## Enable Routing Recommended first-run setup: @@ -66,9 +122,9 @@ OpenRouter and TokenRhythm use the following recommended single-model tiers: | Tier | OpenRouter | TokenRhythm | | --- | --- | --- | | C0 | `qwen/qwen3.7-flash` | `qwen3.7-flash` | -| C1 | `deepseek/deepseek-v4-flash-0731` | `deepseek-flash` | -| C2 | `deepseek/deepseek-v4-pro-0813` | `deepseek-v4-pro-0813` | -| C3 | `z-ai/glm-5.3` | `glm-5.3` | +| C1 | `deepseek/deepseek-v4-flash` | `deepseek-v4-flash-0731` | +| C2 | `deepseek/deepseek-v4-pro` | `deepseek-v4-pro-0813` | +| C3 | `moonshotai/kimi-k3` | `kimi-k3` | The TokenRhythm ladder leaves tier `thinking_level` unset. Direct requests without an explicit thinking setting preserve the provider default; @@ -100,6 +156,10 @@ without submitting tier selections does not enable cross-provider execution. Existing primary-switch and legacy-router conflict checks still apply. Dormant single-model settings under a static C3 fusion plan do not enable it either. +### Legacy V4 Deployment Behavior + +The following fusion and image-fallback behavior applies to `legacy_v4_phase3`. + C3 can optionally use the shared multi-model fusion plan configured under `llm_ensemble`. Enabling fusion on C3 makes it use that plan instead of its single-model route. If the plan falls back to a single model, it uses the global @@ -136,6 +196,8 @@ tool effects. Projection never deletes the canonical attachment, so switching back to a vision-capable model can recover earlier images after history compression. +### Direct Mode + Disable routing and use the configured provider/model directly: ```sh @@ -166,11 +228,11 @@ Zhipu, and compatible provider tiers exposed by the local catalog. Depending on configuration, SquillaRouter may influence: - selected model tier; -- direct model fallback; +- direct model fallback in the legacy V4 strategy; - reasoning level; - response policy; -- image-capable model selection; -- cache-continuity safeguards for recent higher-tier turns. +- image-capability admission, or image-capable tier selection in legacy V4; +- cache-continuity safeguards for recent higher-tier turns in legacy V4. The exact decision is available through runtime metadata and diagnostics surfaces. Turn on diagnostics when you need to understand why a turn was routed @@ -248,8 +310,10 @@ If routing does not appear to work: opensquilla doctor ``` -3. If SquillaRouter optional dependencies are missing, OpenSquilla can still run - with direct single-model routing. On Windows, ONNX Runtime may require the +3. The model policy fails closed on missing dependencies or invalid assets. + Install the recommended dependencies and restore the model resources, or + explicitly disable routing to use direct single-model execution. + On Windows, ONNX Runtime may require the Visual C++ Redistributable. On macOS terminal installs, LightGBM may require `libomp` from Homebrew: diff --git a/opensquilla.toml.example b/opensquilla.toml.example index db438a7688..90a5d3280a 100644 --- a/opensquilla.toml.example +++ b/opensquilla.toml.example @@ -398,7 +398,8 @@ max_pending_per_session = 64 enabled = true # V4 model router auto_thinking = true rollout_phase = "full" -strategy = "v4_phase3" +strategy = "lightgbm_model" +tier_policy = "model" # Optional provider tier profile. Leave unset to preserve the built-in # TokenRhythm defaults below. If set, it must match [llm].provider; the router # does not switch providers at runtime. @@ -435,8 +436,8 @@ supports_image = false [squilla_router.tiers.c1] provider = "tokenrhythm" -model = "deepseek-flash" -description = "Default DeepSeek Flash route for normal agent work, coding assistance, debugging, and moderate analysis" +model = "deepseek-v4-flash-0731" +description = "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis" supports_image = false [squilla_router.tiers.c2] @@ -447,8 +448,8 @@ supports_image = false [squilla_router.tiers.c3] provider = "tokenrhythm" -model = "glm-5.3" -description = "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" +model = "kimi-k3" +description = "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" supports_image = false ensemble_enabled = false diff --git a/pyproject.toml b/pyproject.toml index 4cf9a66519..8f99779a33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,8 +105,10 @@ recommended = [ "numpy>=2.5.3", "lightgbm>=4.7.0", "joblib>=1.6.0", - "scikit-learn>=1.9.1", - "onnxruntime>=1.30.0", + # Frozen transformers were persisted with scikit-learn 1.8.0. + "scikit-learn==1.8.0", + # Newer ORT changes the frozen BGE features; 1.24.4 matches reference bytes. + "onnxruntime==1.24.4", "tokenizers>=0.23.2", "fsspec>=2026.6.0", # Standard installations include the managed execution PTY backend. The @@ -221,6 +223,8 @@ markers = [ [tool.ruff] target-version = "py312" line-length = 100 +# Extracted upstream inference definitions retain their original formatting. +extend-exclude = ["src/opensquilla/squilla_router/lightgbm_model/_vendor"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] @@ -250,10 +254,12 @@ module = [ "dingtalk_stream.*", "extract_msg.*", "jieba", + "joblib.*", "nio.*", "numpy.*", "onnxruntime.*", "readability.*", + "sklearn.*", "src.router.*", "tiktoken.*", "tokenizers.*", diff --git a/scripts/build_wheelhouse_zip.py b/scripts/build_wheelhouse_zip.py index eb0acf5c9e..ebb9384614 100644 --- a/scripts/build_wheelhouse_zip.py +++ b/scripts/build_wheelhouse_zip.py @@ -54,6 +54,13 @@ "opensquilla/skills/bundled/pptx/references/python_pptx.md", } ) +LIGHTGBM_ASSET_PREFIX = "opensquilla/squilla_router/lightgbm_model/assets" +LIGHTGBM_ASSET_RELS = ( + "artifact_manifest.json", + "model_manifest.json", + "intent/model.txt.gz", + "tier/model.txt.gz", +) ROUTER_ASSET_RELS = ( "v4.2_phase3_inference/PROVENANCE.md", "v4.2_phase3_inference/artifact_manifest.json", @@ -62,6 +69,7 @@ "v4.2_phase3_inference/mlp/model.onnx", "v4.2_phase3_inference/features/tfidf.pkl", "v4.2_phase3_inference/bge_onnx/model.onnx", + "v4.2_phase3_inference/bge_onnx/tokenizer.json", ) REQUIRED_RUNTIME_MODULE_RELS = ( "opensquilla/cli/main.py", @@ -262,19 +270,66 @@ def python_runtime_asset_url(asset_name: str, runtime_release: str) -> str: def required_router_assets(model_root: Path) -> tuple[Path, ...]: - return tuple(model_root / rel for rel in ROUTER_ASSET_RELS) + return ( + *(model_root / rel for rel in ROUTER_ASSET_RELS), + *(model_root.parent / "lightgbm_model" / "assets" / rel for rel in LIGHTGBM_ASSET_RELS), + ) def check_router_assets(model_root: Path) -> RouterAssetCheck: missing_files: list[Path] = [] pointer_files: list[Path] = [] - for path in required_router_assets(model_root): + paths = list(required_router_assets(model_root)) + records: dict[Path, dict] = {} + for asset_root in ( + (model_root / "v4.2_phase3_inference").resolve(), + (model_root.parent / "lightgbm_model/assets").resolve(), + ): + manifest_path = asset_root / "artifact_manifest.json" + if manifest_path.is_file(): + manifest_bytes = manifest_path.read_bytes() + if not manifest_bytes.startswith(LFS_POINTER_LINE.encode()): + try: + manifest = json.loads(manifest_bytes) + if manifest["schema_version"] != 1 or not manifest["files"]: + raise ValueError("unsupported or empty asset manifest") + for record in manifest["files"]: + relative = record["path"] + path = (asset_root / relative).resolve() + if ( + not isinstance(relative, str) + or "\\" in relative + or Path(relative).is_absolute() + or ".." in Path(relative).parts + or path == asset_root + or not path.is_relative_to(asset_root) + or path in records + or not isinstance(record["size_bytes"], int) + or isinstance(record["size_bytes"], bool) + or record["size_bytes"] < 0 + or len(record["sha256"]) != 64 + or any(char not in "0123456789abcdef" for char in record["sha256"]) + ): + raise ValueError("unsafe or incomplete asset record") + records[path] = record + paths.append(path) + except (ValueError, KeyError, TypeError, AttributeError) as exc: + raise SystemExit(f"Invalid router artifact manifest: {manifest_path}") from exc + for path in dict.fromkeys(paths): if not path.is_file(): missing_files.append(path) continue - first_line = path.read_text(encoding="utf-8", errors="ignore").splitlines() - if first_line and first_line[0].strip() == LFS_POINTER_LINE: + with path.open("rb") as handle: + prefix = handle.read(len(LFS_POINTER_LINE)) + if prefix == LFS_POINTER_LINE.encode(): pointer_files.append(path) + continue + record = records.get(path.resolve()) + if record is not None: + with path.open("rb") as handle: + digest = hashlib.file_digest(handle, "sha256").hexdigest() + if path.stat().st_size != record["size_bytes"] or digest != record["sha256"]: + raise SystemExit(f"Router artifact integrity mismatch: {path}") return RouterAssetCheck(tuple(missing_files), tuple(pointer_files)) @@ -299,7 +354,10 @@ def _is_allowed_runtime_markdown(path: str) -> bool: and parts[1:] == ["licenses", "THIRD_PARTY_NOTICES.md"] ): return True - if name == ROUTER_PROVENANCE_WHEEL_PATH: + if name in { + ROUTER_PROVENANCE_WHEEL_PATH, + "opensquilla/squilla_router/lightgbm_model/PROVENANCE.md", + }: return True if name == TOKENJUICE_PROVENANCE_WHEEL_PATH: return True @@ -353,8 +411,15 @@ def forbidden_release_text_hits(wheel_path: Path) -> list[str]: def missing_router_assets_in_wheel(wheel_path: Path) -> list[str]: expected = {f"{WHEEL_ROUTER_PREFIX}/{rel}" for rel in ROUTER_ASSET_RELS} + expected.update(f"{LIGHTGBM_ASSET_PREFIX}/{rel}" for rel in LIGHTGBM_ASSET_RELS) with ZipFile(wheel_path) as archive: names = set(archive.namelist()) + manifest_path = f"{LIGHTGBM_ASSET_PREFIX}/artifact_manifest.json" + if manifest_path in names: + manifest = json.loads(archive.read(manifest_path)) + expected.update( + f"{LIGHTGBM_ASSET_PREFIX}/{entry['path']}" for entry in manifest["files"] + ) return sorted(expected - names) diff --git a/scripts/compare_gateway_timing.py b/scripts/compare_gateway_timing.py index f73aef9435..8646b536b5 100644 --- a/scripts/compare_gateway_timing.py +++ b/scripts/compare_gateway_timing.py @@ -1,7 +1,9 @@ -"""Opt-in same-interpreter comparison using the existing offline Gateway fixture. +"""Opt-in same-Python-version comparison using the offline Gateway fixture. Source directories must already exist. This tool never creates checkouts or reads -provider credentials. Durations cover process-to-health and Default send-to-done, +provider credentials. By default dependency inputs must match; an explicit +baseline interpreter allows each source to use its own locked environment. +Durations cover process-to-health and Default send-to-done, including local client/runtime overhead; they are not model TTFT measurements. """ @@ -43,14 +45,18 @@ def verify_source_identity(root: Path, sha: str, *, archived: bool = False) -> N raise ValueError("source must be the requested exact clean Git checkout") -def validate_sources(baseline: Path, candidate: Path, samples: int) -> None: +def validate_sources( + baseline: Path, candidate: Path, samples: int, *, separate_environments: bool = False, +) -> None: if not 1 <= samples <= 10: raise ValueError("samples must be between 1 and 10") for root in (baseline, candidate): if not (root / "src/opensquilla/gateway/boot.py").is_file(): raise ValueError("source root must contain the Gateway package") for name in ("uv.lock", "pyproject.toml"): - if (baseline / name).read_bytes() != (candidate / name).read_bytes(): + if not separate_environments and ( + (baseline / name).read_bytes() != (candidate / name).read_bytes() + ): raise ValueError(f"comparison requires identical {name}") @@ -68,14 +74,55 @@ def sample_environment(root: Path, profile: Path) -> dict[str, str]: return env +def _dependency_inputs(root: Path) -> dict[str, str]: + return { + name: hashlib.sha256((root / name).read_bytes()).hexdigest() + for name in ("uv.lock", "pyproject.toml") + } + + +def _interpreter_identity(executable: str) -> dict[str, Any]: + # Isolated startup excludes inherited PYTHONPATH and user-site packages. + # Prefix is compared privately and never included in the report. + code = ( + "import json,platform,sys; " + "print(json.dumps({'version':list(sys.version_info[:3])," + "'implementation':sys.implementation.name,'machine':platform.machine()," + "'prefix':sys.prefix}))" + ) + return json.loads(subprocess.check_output([executable, "-I", "-c", code], text=True)) + + def compare( *, baseline: Path, candidate: Path, baseline_sha: str, candidate_sha: str, samples: int, + baseline_python: Path | None = None, ) -> dict[str, Any]: - validate_sources(baseline, candidate, samples) + validate_sources( + baseline, candidate, samples, separate_environments=baseline_python is not None, + ) if not all(re.fullmatch(r"[0-9a-f]{40}", sha) for sha in (baseline_sha, candidate_sha)): raise ValueError("source SHAs must be full lowercase commit hashes") verify_source_identity(baseline, baseline_sha, archived=True) verify_source_identity(candidate, candidate_sha) + dependency_inputs = { + label: _dependency_inputs(root) + for label, root in (("baseline", baseline), ("candidate", candidate)) + } + interpreters = {"baseline": str(baseline_python or sys.executable), "candidate": sys.executable} + identities = { + label: _interpreter_identity(executable) for label, executable in interpreters.items() + } + for field in ("version", "implementation", "machine"): + if identities["baseline"][field] != identities["candidate"][field]: + raise ValueError(f"comparison requires identical Python {field}") + if ( + dependency_inputs["baseline"] != dependency_inputs["candidate"] + and Path(identities["baseline"]["prefix"]).resolve() + == Path(identities["candidate"]["prefix"]).resolve() + ): + raise ValueError("different dependency inputs require separate Python environments") + for identity in identities.values(): + identity.pop("prefix") harness = candidate / "tests/functional/test_gateway_silent_reply_process_e2e.py" rows: dict[str, list[dict[str, float | int]]] = {"baseline": [], "candidate": []} with tempfile.TemporaryDirectory(prefix="opensquilla-gateway-timing-") as temporary: @@ -88,7 +135,7 @@ def compare( profile = scratch / f"{label}-{index}" env = sample_environment(root, profile) result = subprocess.run( - [sys.executable, str(harness), "--sample-dir", str(profile / "sample"), + [interpreters[label], str(harness), "--sample-dir", str(profile / "sample"), "--source-root", str(root)], cwd=root, env=env, capture_output=True, text=True, timeout=90, ) @@ -114,6 +161,8 @@ def compare( } return { "baseline_sha": baseline_sha, "candidate_sha": candidate_sha, + "dependency_mode": "per-source-environments" if baseline_python else "shared-environment", + "dependency_inputs": dependency_inputs, "interpreters": identities, "uv_lock_sha256": hashlib.sha256((candidate / "uv.lock").read_bytes()).hexdigest(), "pyproject_sha256": hashlib.sha256((candidate / "pyproject.toml").read_bytes()).hexdigest(), "samples_per_source": samples, "samples": rows, "median_ms": medians, @@ -130,12 +179,17 @@ def main() -> None: parser.add_argument("--candidate-root", type=Path, required=True) parser.add_argument("--baseline-sha", required=True) parser.add_argument("--candidate-sha", required=True) + parser.add_argument( + "--baseline-python", type=Path, + help="Baseline's separately installed locked environment (same Python version)", + ) parser.add_argument("--samples", type=int, default=3) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() report = compare( baseline=args.baseline_root.resolve(), candidate=args.candidate_root.resolve(), baseline_sha=args.baseline_sha, candidate_sha=args.candidate_sha, samples=args.samples, + baseline_python=args.baseline_python, ) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") diff --git a/scripts/install_source.ps1 b/scripts/install_source.ps1 index c2ee9e95d4..06459b77fb 100644 --- a/scripts/install_source.ps1 +++ b/scripts/install_source.ps1 @@ -135,6 +135,9 @@ function Test-SquillaRouterAssets { $modelRoot = Join-Path $sourceRoot 'src/opensquilla/squilla_router/models' $required = @( + "$sourceRoot/src/opensquilla/squilla_router/lightgbm_model/assets/artifact_manifest.json", + "$sourceRoot/src/opensquilla/squilla_router/lightgbm_model/assets/intent/model.txt.gz", + "$sourceRoot/src/opensquilla/squilla_router/lightgbm_model/assets/tier/model.txt.gz", "$modelRoot/v4.2_phase3_inference/lgbm_main.bin", "$modelRoot/v4.2_phase3_inference/router.runtime.yaml", "$modelRoot/v4.2_phase3_inference/mlp/model.onnx", @@ -171,7 +174,7 @@ function Test-SquillaRouterAssets { $message = "install_source.ps1: Git LFS pointer files detected: $($pointers -join ', ')" if ($WarnOnly) { Write-Host $message } else { Write-Error $message } } - $lfsMessage = 'install_source.ps1: run `git lfs install` once, then `git lfs pull --include="src/opensquilla/squilla_router/models/**"`.' + $lfsMessage = 'install_source.ps1: run `git lfs install` once, then `git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**"`.' $coreMessage = 'install_source.ps1: or retry with `$env:OPENSQUILLA_INSTALL_PROFILE="core"` for the minimal runtime.' if ($WarnOnly) { Write-Host $lfsMessage diff --git a/scripts/install_source.sh b/scripts/install_source.sh index 73de2df764..5fdbd14df9 100755 --- a/scripts/install_source.sh +++ b/scripts/install_source.sh @@ -150,6 +150,9 @@ check_squilla_router_assets() { local model_root="${source_root}/src/opensquilla/squilla_router/models" local pointer_line="version https://git-lfs.github.com/spec/v1" local required=( + "${source_root}/src/opensquilla/squilla_router/lightgbm_model/assets/artifact_manifest.json" + "${source_root}/src/opensquilla/squilla_router/lightgbm_model/assets/intent/model.txt.gz" + "${source_root}/src/opensquilla/squilla_router/lightgbm_model/assets/tier/model.txt.gz" "${model_root}/v4.2_phase3_inference/lgbm_main.bin" "${model_root}/v4.2_phase3_inference/router.runtime.yaml" "${model_root}/v4.2_phase3_inference/mlp/model.onnx" @@ -181,7 +184,7 @@ check_squilla_router_assets() { echo "install_source.sh: Git LFS pointer files detected: ${pointers[*]}" >&2 fi echo 'install_source.sh: run `git lfs install` once, then:' >&2 - echo 'install_source.sh: git lfs pull --include="src/opensquilla/squilla_router/models/**"' >&2 + echo 'install_source.sh: git lfs pull --include="src/opensquilla/squilla_router/models/**,src/opensquilla/squilla_router/lightgbm_model/assets/**"' >&2 echo 'install_source.sh: or retry with OPENSQUILLA_INSTALL_PROFILE=core for the minimal runtime.' >&2 if [[ "${mode}" == "warn" ]]; then return 0 diff --git a/scripts/live_long_task_case_driver.py b/scripts/live_long_task_case_driver.py index 72873325cc..d97451025c 100644 --- a/scripts/live_long_task_case_driver.py +++ b/scripts/live_long_task_case_driver.py @@ -530,7 +530,9 @@ def render_gateway_config( f"cross_provider_tiers = {'true' if is_fallback else 'false'}", 'tier_provider_mismatch = "veto"', 'rollout_phase = "full"', - 'strategy = "v4_phase3"', + # Fallback is a legacy-router acceptance scenario; the default model + # policy deliberately rejects fallback and has independent regression coverage. + f'strategy = "{"legacy_v4_phase3" if is_fallback else "model"}"', 'default_tier = "c0"', "require_router_runtime = false", ] diff --git a/scripts/live_provider_profile_gateway_e2e.py b/scripts/live_provider_profile_gateway_e2e.py index c6cf2dc2cb..342d92c1e0 100644 --- a/scripts/live_provider_profile_gateway_e2e.py +++ b/scripts/live_provider_profile_gateway_e2e.py @@ -388,6 +388,7 @@ def _write_config( turn_hard_deadline_seconds: float = LIVE_TURN_HARD_DEADLINE_SECONDS, llm_context_window_tokens: int | None = None, model_supports_vision_override: str | None = None, + router_strategy: str = "model", ) -> None: tier_override_toml = _render_tier_overrides(tier_overrides) llm_thinking_toml = ( @@ -473,7 +474,7 @@ def _write_config( enabled = true auto_thinking = true rollout_phase = "full" -strategy = "v4_phase3" +strategy = "{router_strategy}" {tier_profile_toml} default_tier = "{default_tier}" confidence_threshold = 0.5 @@ -1667,6 +1668,8 @@ def _run_tokenrhythm_attachment_capacity_in_temp( model_supports_vision_override=( str(tiers["c2"]["model"]) if synthetic_vision_capability_override else None ), + # This gate verifies the legacy image floor and image_route decision evidence. + router_strategy="legacy_v4_phase3", ) port = _free_port() provider_spec = get_provider_spec(ATTACHMENT_CAPACITY_PROVIDER) diff --git a/src/opensquilla/application/turn_acceptance.py b/src/opensquilla/application/turn_acceptance.py index cb67e0d002..9d4c093e43 100644 --- a/src/opensquilla/application/turn_acceptance.py +++ b/src/opensquilla/application/turn_acceptance.py @@ -91,6 +91,11 @@ async def admit(self, command: AdmitTurn) -> AdmitTurnResult: return await _replay_retired_request(command, ports) try: _ensure_required_services(command, ports) + if command.router_control_event is not None and ( + not isinstance(command.router_control_event, str) + or command.router_control_event not in {"new_task", "redo"} + ): + raise ValueError("routerControlEvent must be new_task or redo") if command.initial_model is not None or command.initial_provider is not None: if command.intent != "new_chat" or not command.intent_was_provided: raise ValueError("initialModel requires explicit new_chat intent") @@ -101,9 +106,11 @@ async def admit(self, command: AdmitTurn) -> AdmitTurnResult: or command.initial_routing_mode or command.initial_model is not None or command.initial_provider is not None + or command.router_control_event is not None ): raise AdmissionUnavailableError( - "Initial session controls require atomic turn acceptance" + "Initial session controls and router controls require " + "atomic turn acceptance" ) return { "ok": True, @@ -1021,7 +1028,10 @@ def _cleanup_rejected_guest_profile() -> None: route_envelope = ports.refine_route( route_envelope, metadata={ - **route_envelope.metadata, + **{ + name: value for name, value in route_envelope.metadata.items() + if name != "router_model_control_event" + }, "client_request_id": ingress_identity.client_request_id, "client_message_id": client_message_id, "surface_id": surface_id, @@ -1031,6 +1041,10 @@ def _cleanup_rejected_guest_profile() -> None: "task_id": turn_id, "turn_context_intent": "send", "turn_context_revision": 1, + **( + {"router_model_control_event": command.router_control_event} + if command.router_control_event is not None else {} + ), **( { "plan_run_id": plan_run.run_id, @@ -1094,6 +1108,10 @@ def _cleanup_rejected_guest_profile() -> None: "intent": "send", "disposition": "queued" if ports.runtime is not None else "applied", "revision": 1, + **( + {"router_model_control_event": command.router_control_event} + if command.router_control_event is not None else {} + ), "sandbox_mode_resolution": { "desiredMode": mode_resolution.desired_mode.value, "effectiveMode": mode_resolution.effective_mode.value, @@ -1133,7 +1151,10 @@ async def _run_direct_turn() -> None: task_runtime = task_runtime_candidate requested_mode = command.queue_mode or getattr(session, "queue_mode", None) or "followup" - if requested_mode == "steer" and not command.selected_skills: + if ( + requested_mode == "steer" and not command.selected_skills + and command.router_control_event is None + ): log.info( "sessions.send.legacy_steer_queue_mode_used", session_key=key, @@ -1142,8 +1163,9 @@ async def _run_direct_turn() -> None: replacement="sessions.steer.v2", ) ports.steer_metric("legacy_interrupt_requested", session_key=key) + # A task control owns one routing decision; collection or live steering would lose it. runtime_mode = ( - "followup" if command.selected_skills + "followup" if command.selected_skills or command.router_control_event is not None else "interrupt" if requested_mode == "steer" else requested_mode ) if atomic_intent_plan is not None and atomic_intent_plan.action == "reset": @@ -2134,7 +2156,7 @@ async def _rollback_persisted_user_message(reason: str) -> tuple[str | None, boo if task_runtime is not None: requested_mode = command.queue_mode or getattr(session, "queue_mode", None) or "followup" runtime_mode = ( - "followup" if command.selected_skills + "followup" if command.selected_skills or command.router_control_event is not None else "interrupt" if requested_mode == "steer" else requested_mode ) try: diff --git a/src/opensquilla/application/turn_admission.py b/src/opensquilla/application/turn_admission.py index 60b7019302..bf8dfb9da5 100644 --- a/src/opensquilla/application/turn_admission.py +++ b/src/opensquilla/application/turn_admission.py @@ -26,6 +26,7 @@ type TurnAdmissionSurface = Literal["webchat", "session"] type InitialCollaborationMode = Literal["default", "plan"] type InitialRoutingMode = Literal["direct", "router", "ensemble"] +type RouterControlEvent = Literal["new_task", "redo"] class AcceptedCollaboration(TypedDict): @@ -163,6 +164,8 @@ class AdmitTurn: initial_routing_mode: InitialRoutingMode | None = None initial_model: str | None = None initial_provider: str | None = None + # Trusted structured control, independent of message text and session lifecycle. + router_control_event: RouterControlEvent | None = None pending_input: PendingInputGuard | None = None # Optional external capabilities that must be ready before durable # acceptance. This is an internal producer hint: ordinary chat remains diff --git a/src/opensquilla/engine/agent.py b/src/opensquilla/engine/agent.py index 9269c06612..d2318ced68 100644 --- a/src/opensquilla/engine/agent.py +++ b/src/opensquilla/engine/agent.py @@ -238,6 +238,7 @@ ExecutionIdentity, FailureInjector, ModelCapabilities, + ProviderCompatibilityRecovery, ProviderFinalRequestProjection, ProviderMessageCountProjection, ProviderMessageLimitProof, @@ -6277,6 +6278,49 @@ async def _turn_generator( self._compaction_request_context = self.build_compaction_request_context( thinking_prompt ) + from opensquilla.engine.model_policy_recovery import ( + ModelPolicyRecovery, + effective_write_count, + validation_failure, + ) + + model_policy_recovery = ModelPolicyRecovery( + self.config, + coordinate_retries=( + getattr(self.provider, "coordinates_physical_retries", False) is True + ), + ) + pending_model_quality_retry: str | None = None + + async def _try_model_quality_retry( + reason: str, messages: list[Message], tools: list[ToolDefinition] | None, + ) -> bool: + nonlocal chat_cfg, thinking_enabled, thinking_budget + updates = await model_policy_recovery.prepare( + self.provider, reason, messages, chat_config=chat_cfg, tools=tools, + ) + if updates is None: + return False + # Callback has preflighted and durably committed the effective tier. + # Keep this turn's messages and provider wrapper across the handoff. + for name, value in updates.items(): + setattr(self.config, name, value) + thinking_enabled, thinking_budget = self.config.resolve_thinking(prompt=thinking_prompt) + chat_cfg = self._provider_admission_chat_config( + thinking_prompt, + context_window_tokens=self.config.context_window_tokens, + max_output_tokens=self.config.max_tokens, + ) + self._compaction_request_context = self.build_compaction_request_context( + thinking_prompt + ) + self._write_turn_call_log( + "model_policy_quality_retry", action="apply", reason=reason, + model=self.config.model_id, provider=self.config.provider_id, + budget=model_policy_recovery.budget, used=model_policy_recovery.used, + ) + return True + _log = structlog.get_logger("opensquilla.engine.agent") def _positive_float(value: Any) -> float | None: @@ -7052,9 +7096,12 @@ async def _mark_staged_pending_inputs_applied( _retry_attempt = 0 stream_replay_request: StreamRecoveryRequest | None = None + compatibility_recovery: ProviderCompatibilityRecovery | None = None + compatibility_recoveries: set[ProviderCompatibilityRecovery] = set() stream_recovery_used = False _connection_wait_attempt = 0 _call_attempt = 0 + dispatch_evidence: RetryAfterDispatchEvidence | None = None _retry_policy = _ProviderRetryPolicy.from_provider_budget( _fallback.max_retries, length_capped_continuations=self.config.length_capped_continuations, @@ -7293,6 +7340,51 @@ def _finish_reasoning_block( self._provider_call_tool_result_retrieval_available = ( previous_call_retrieval ) + # A quality upgrade must not commit when this call is + # already forbidden by the turn's execution budget. + terminal_error = ( + _turn_llm_call_budget_error(turn_llm_calls + 1) or _turn_budget_error() + ) + if terminal_error is not None: + self._write_turn_call_log( + "turn_policy_decision", + action=("stop"), + reason=terminal_error.message, + code=terminal_error.code, + sent_llm_calls=turn_llm_calls, + attempted_llm_call=turn_llm_calls + 1, + iteration=iterations, + attempt=_call_attempt, + ) + yield self._transition(AgentState.ERROR) + yield terminal_error + break + + if pending_model_quality_retry is not None: + if selector_projects_images: + # Bind this iteration's barrier before candidate + # preview; marker forcing remains deployment-bound. + selector_image_provider.configure_image_request_projection( + force_marker=( + image_projection_forced + and selector_image_provider.active_deployment_config() + == image_projection_forced_deployment + ), + marker_state=image_projection_marker_state, + forbid_unknown_probe=turn_image_retry_barrier_crossed, + reason=( + str(self.config.metadata.get("image_input_reason") or "") + or None + ), + ) + reason = pending_model_quality_retry + pending_model_quality_retry = None + # Admit the fully assembled next request, including + # transient context and provider-view transformations, + # before committing or activating the higher tier. + await _try_model_quality_retry( + reason, request_messages, provider_tools_for_call, + ) # Project only this physical request. ``turn_messages`` and # ``request_turn_messages`` remain image-bearing canonical # views so a later vision-capable turn can recover the @@ -7482,23 +7574,6 @@ def _finish_reasoning_block( sanitize=request_sanitize_result, ) - next_call_budget_error = _turn_llm_call_budget_error(turn_llm_calls + 1) - terminal_error = next_call_budget_error - if terminal_error is not None: - self._write_turn_call_log( - "turn_policy_decision", - action=("stop"), - reason=terminal_error.message, - code=terminal_error.code, - sent_llm_calls=turn_llm_calls, - attempted_llm_call=turn_llm_calls + 1, - iteration=iterations, - attempt=_call_attempt, - ) - yield self._transition(AgentState.ERROR) - yield terminal_error - break - call_chat_cfg = chat_cfg if tool_failure_finalization_pending: call_chat_cfg = call_chat_cfg.model_copy( @@ -7888,6 +7963,30 @@ async def _recover_pending_cooling_window(reason: str) -> bool: attempt=_call_attempt, messages=request_messages, tools=provider_tools_for_call, config=call_chat_cfg, ) + # Preparation can loop before any provider stream is opened. + # Only a prior dispatch consumes an additional-call allowance; + # explicit local cooling is likewise not a physical attempt. + retry_dispatch = dispatch_evidence is not None and ( + dispatch_evidence.physical_started or not dispatch_evidence.cooling_observed + ) + if retry_dispatch and not model_policy_recovery.reserve_transport_retry(): + terminal_error = ErrorEvent( + message="Provider retry exhausted the shared additional-call budget.", + code="provider_retry_budget_exhausted", + no_prior_provider_dispatch=False, + replay_safe=False, + ) + yield self._transition(AgentState.ERROR) + yield terminal_error + break + if compatibility_recovery is not None: + call_chat_cfg = call_chat_cfg.model_copy(update={ + "compatibility_recovery": compatibility_recovery, + }) + if compatibility_recovery == "refresh_credentials": + # Refresh at most once; subsequent transport retries use + # the already persisted credential, never another refresh. + compatibility_recovery = None turn_llm_calls += 1 cache_prompt_snapshot = None if self._session_key: @@ -10502,6 +10601,31 @@ def _active_stream_deadline() -> float | None: failure_kind.value, ) kind = failure_kind + recovery_action = provider_error.compatibility_recovery + if ( + model_policy_recovery.owns_retry_budget + and recovery_action is not None + and recovery_action in {"non_stream", "refresh_credentials"} + and recovery_action not in compatibility_recoveries + and _retry_attempt < _fallback.max_retries + and not provider_error.recovery_exhausted + and not attempt_irreversible_output_emitted + and not attempt_user_visible_emitted + and not _got_done_event + and provider_stream_closed and not close_evidence.failed + and getattr(self.provider, "retry_failed_call_safe", True) is not False + ): + compatibility_recovery = recovery_action + compatibility_recoveries.add(recovery_action) + _retry_attempt += 1 + self._write_turn_call_log( + "provider_compatibility_recovery", action=recovery_action, + attempt=_retry_attempt, model=self._provider_activity_model(), + ) + # The next dispatch reserves the shared budget before + # either refreshing credentials or issuing a model call. + _call_attempt += 1 + continue if ( provider_error.code == "provider_protocol_error" and not protocol_recovery_used @@ -12175,6 +12299,37 @@ def capacity_fallback_event() -> ProviderActivityEvent | None: yield terminal_error break + repeat_message = model_policy_recovery.repeated_call_message( + tool_calls, effective_write_count(self._workspace_write_records()) + ) + if repeat_message is not None: + # The duplicate was fully received but is never dispatched; + # discard its buffered tool UI events and avoid an orphaned + # assistant tool-use row in the canonical transcript. + if self._execution_context is not None: + self._execution_context.drop_pending_tool_buffers("model_policy_no_progress") + for preview in discard_tool_previews(): + yield preview + if response_text: + turn_messages.append(Message( + role="assistant", content=response_text, + reasoning_content=iter_reasoning_content, + )) + turn_messages.append(Message(role="user", content=repeat_message)) + runtime_recovery_scaffolding_pending = True + self.config.metadata["repeated_tool_call_recoveries"] = ( + self.config.metadata.get("repeated_tool_call_recoveries", 0) + 1 + ) + pending_model_quality_retry = "no_progress" + yield WarningEvent( + code="repeated_tool_call_recovery", + message=( + "Runtime skipped a repeated identical tool call and " + "asked for a different approach." + ), + ) + continue + # Tool events are transactional with the provider attempt. The # final classification has already established that a DoneEvent # was received and that every tool sequence was closed and @@ -13375,6 +13530,9 @@ async def _run_after_key_lock() -> ToolResult: iteration=iterations, tool_use_ids=sorted(preflight_tool_results), ) + model_policy_recovery.observe_results(tool_calls, executed_results) + if not turn_yielded and validation_failure(executed_results): + pending_model_quality_retry = "validation_failure" if turn_yielded: break if tool_failure_recovery.exhausted: @@ -18026,6 +18184,7 @@ async def _subagent_tool_handler(tc: ToolCall) -> ToolResult: provider_id=child_target.provider_id, model_id=child_target.model_id or self.config.model_id, max_tokens=child_target.max_output_tokens, + max_provider_retries=self.config.max_provider_retries, temperature=self.config.temperature, top_p=self.config.top_p, max_turn_llm_calls=self.config.max_turn_llm_calls, diff --git a/src/opensquilla/engine/model_policy_recovery.py b/src/opensquilla/engine/model_policy_recovery.py new file mode 100644 index 0000000000..7b97668d02 --- /dev/null +++ b/src/opensquilla/engine/model_policy_recovery.py @@ -0,0 +1,189 @@ +"""Bounded execution recovery for the intent-led model routing policy. + +The callback owns deployment preflight, durable effective-tier CAS, and provider +activation. This helper owns signal qualification and the Agent's turn-wide +additional-call budget shared by transport retries and the quality remedy. +""" + +from __future__ import annotations + +import json +import uuid +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +from opensquilla.engine.routing.model_policy import quality_retry_tier +from opensquilla.provider.types import ChatConfig, ToolDefinition + +VALIDATION_REASONS = frozenset( + { + "invalid_arguments", + "retryable_tool_input_error", + "schema_validation_failed", + "output_validation_failed", + "invalid_tool_arguments", + "invalid_json", + "format_validation_failed", + } +) +_REPEAT_TOOLS = frozenset({"exec_command", "glob_search", "grep_search", "list_dir"}) +_SYNTHETIC_WRITE_PREFIXES = ("debug_marker", "guard_unlock", "runtime_guard", "temp_marker") + + +def effective_write_count(records: Sequence[Mapping[str, Any]]) -> int: + """Retain the frozen no-progress guard's synthetic-marker exclusion.""" + count = 0 + for record in records: + name = Path( + str(record.get("relative_path") or record.get("path") or "") + .replace("\\", "/") + .lstrip("./") + ).name.lower() + synthetic = bool(record.get("created")) and any( + name == prefix or name.startswith(f"{prefix}.") or name.startswith(f"{prefix}_") + for prefix in _SYNTHETIC_WRITE_PREFIXES + ) + count += not synthetic + return count + + +def validation_failure(results: Sequence[Any]) -> bool: + return any( + result.is_error + and isinstance(result.execution_status, Mapping) + and result.execution_status.get("reason") in VALIDATION_REASONS + for result in results + ) + + +class ModelPolicyRecovery: + def __init__(self, config: Any, *, coordinate_retries: bool = False) -> None: + self.metadata = config.metadata + self.context_overflow_threshold = float(config.context_overflow_threshold) + self.enabled = ( + self.metadata.get("router_tier_policy") == "model" + and self.metadata.get("routing_applied") is True + ) + # A cloned child owns its own finite budget, without inheriting the + # parent's routing decision, quality callback or durable session state. + self.owns_retry_budget = self.enabled or coordinate_retries + self.budget = min(3, max(0, int(config.max_provider_retries or 0))) + self.used = 0 + self.quality_attempted = False + self.scope_id = f"model-policy-turn:{uuid.uuid4().hex}" + self.repeat_threshold = max(0, int(config.repeated_tool_call_recovery_threshold or 0)) + self.repeat_tools = _REPEAT_TOOLS | set( + config.repeated_tool_call_recovery_extra_tools or () + ) + self.repeat_key: tuple[str, str] | None = None + self.repeat_count = 0 + self.repeat_write_count = 0 + self.repeat_last_error = False + + def reserve_transport_retry(self) -> bool: + if not self.owns_retry_budget: + return True + if self.used >= self.budget: + return False + self.used += 1 + return True + + async def prepare( + self, provider: Any, reason: str, messages: list[Any], *, + chat_config: ChatConfig | None = None, + tools: list[ToolDefinition] | None = None, + ) -> dict[str, Any] | None: + callback = getattr(provider, "prepare_quality_retry", None) + tier = self.metadata.get("routed_tier") + if ( + not self.enabled + or self.quality_attempted + or not callable(callback) + or tier not in {"c0", "c1", "c2", "c3"} + or quality_retry_tier( + tier, + reason, + self.budget - self.used, + held_tier=tier + if self.metadata.get("router_control_hold_applied") is True + else None, + ) + is None + ): + return None + # A refused preflight is not repeatedly attempted later in this turn. + self.quality_attempted = True + request = { + "reason": reason, + "scope_id": self.scope_id, + "remaining_budget": self.budget - self.used, + "messages": messages, + "context_overflow_threshold": self.context_overflow_threshold, + } + if chat_config is not None: + request.update(chat_config=chat_config, tools=tools) + updates = await callback(**request) + if updates is None: + return None + if not isinstance(updates, dict) or set(updates) - { + "model_id", + "provider_id", + "max_tokens", + "context_window_tokens", + "context_window_known", + "model_capabilities", + "model_vision_support", + "thinking", + "thinking_budget_tokens", + }: + raise ValueError("model policy quality callback returned invalid configuration updates") + self.used += 1 + return updates + + @staticmethod + def _key(call: Any) -> tuple[str, str]: + payload = json.dumps(call.arguments, ensure_ascii=False, sort_keys=True, default=str) + return call.tool_name, payload + + def repeated_call_message(self, calls: Sequence[Any], write_count: int) -> str | None: + if not self.enabled: + return None + if ( + len(calls) == 1 + and self.repeat_threshold > 0 + and calls[0].tool_name in self.repeat_tools + ): + key = self._key(calls[0]) + if key == self.repeat_key and write_count == self.repeat_write_count: + self.repeat_count += 1 + else: + self.repeat_key = key + self.repeat_count = 1 + self.repeat_write_count = write_count + self.repeat_last_error = False + if self.repeat_count >= self.repeat_threshold and not self.repeat_last_error: + args = key[1] + if len(args) > 400: + args = args[:397] + "..." + return ( + "[Runtime recovery]\n" + f"The exact same {calls[0].tool_name} tool call has been requested " + f"{self.repeat_count} times in a row with identical arguments. I skipped " + "executing and replaying that duplicate call to avoid provider-side " + "rejection for repetitive tool history. Do not call this exact tool " + "with the same arguments again. Change the path, pattern, command, or " + "arguments; inspect a different source window; use a different tool; " + "or move to the patch/final answer if you already have enough evidence.\n" + f"Repeated arguments: {args}" + ) + elif calls: + self.repeat_key = None + self.repeat_count = 0 + self.repeat_write_count = write_count + self.repeat_last_error = False + return None + + def observe_results(self, calls: Sequence[Any], results: Sequence[Any]) -> None: + if self.enabled and len(calls) == 1 and self._key(calls[0]) == self.repeat_key: + self.repeat_last_error = any(result.is_error for result in results) diff --git a/src/opensquilla/engine/model_router_quality.py b/src/opensquilla/engine/model_router_quality.py new file mode 100644 index 0000000000..391b1058f9 --- /dev/null +++ b/src/opensquilla/engine/model_router_quality.py @@ -0,0 +1,320 @@ +"""One proven, durable quality retry for an already classified model-policy turn.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from dataclasses import dataclass, replace +from typing import Any + +from opensquilla.context_budget import ContextBudgetGovernor +from opensquilla.engine.routing.model_policy import ( + ModelPolicyDecision, + quality_retry_controls, + quality_retry_tier, +) +from opensquilla.engine.types import THINKING_BUDGETS, AgentConfig, ThinkingLevel +from opensquilla.provider.model_catalog import resolve_effective_context_window, shared_catalog +from opensquilla.provider.protocol import ( + project_provider_final_request, + validate_provider_chat_admission, +) +from opensquilla.provider.types import ChatConfig + + +@dataclass(kw_only=True) +class ModelRouterQualityRetry: + turn: Any + selector: Any + manager: Any + input_message_id: str + session_id: str | None + epoch: int | None + + async def __call__( + self, + *, + reason: str, + scope_id: str, + remaining_budget: int, + messages: list[Any], + chat_config: ChatConfig | None, + tools: Any, + activate_prepared: Callable[[Any, Any], None], + prepare_request: Callable[ + [Any, Any, list[Any], Any, ChatConfig], tuple[list[Any], ChatConfig] + ] | None = None, + context_overflow_threshold: float | None = None, + ) -> dict[str, Any] | None: + metadata = self.turn.metadata + envelope = metadata.get("router_model_decision") + if not envelope or not envelope.get("state_committed") or chat_config is None: + return None + state = envelope["state"] + target = quality_retry_tier( + state.get("tier", ""), + reason, + remaining_budget, + already_used=bool(envelope.get("quality_retry", {}).get("used")), + held_tier=(state.get("tier") if metadata.get("router_control_hold_applied") else None), + ) + if target is None: + return None + decision = ModelPolicyDecision.from_dict(envelope["decision"]) + thinking_mode, prompt_policy = quality_retry_controls(decision, target) + router_cfg = self.turn.config.squilla_router + explicit_thinking = str(self.turn.config.llm.thinking or "").strip() + automatic_thinking = router_cfg.auto_thinking and not explicit_thinking + if automatic_thinking: + level = {"T0": "off", "T1": "low", "T2": "medium", "T3": "high"}[thinking_mode] + thinking_level = ThinkingLevel(level) if level != "off" else None + thinking_enabled = level != "off" + thinking_budget = THINKING_BUDGETS[ThinkingLevel(level)] + else: + thinking_level = chat_config.thinking_level + thinking_enabled = chat_config.thinking + thinking_budget = chat_config.thinking_budget_tokens if thinking_enabled else 0 + level = (thinking_level or "medium") if thinking_enabled else "off" + tier_cfg = router_cfg.tiers.get(target) + if not isinstance(tier_cfg, dict) or not tier_cfg.get("model"): + return None + candidate_metadata = { + key: value for key, value in metadata.items() if not key.startswith("routed_provider_") + } + candidate_metadata.update( + routed_tier=target, + routed_model=str(tier_cfg["model"]), + routed_provider=tier_cfg.get("provider") or self.turn.config.llm.provider, + thinking_mode=thinking_mode, + thinking_level=level, + router_fallback_chain=[], + router_fallback_strict=True, + ) + candidate_turn = replace( + self.turn, model=str(tier_cfg["model"]), metadata=candidate_metadata + ) + from opensquilla.engine.routing.policy import budget_gate + from opensquilla.engine.steps import squilla_router as common + from opensquilla.engine.steps.model_router import validate_model_route_capabilities + + budget_cfg = router_cfg.budget + if budget_cfg.limit_usd and budget_cfg.action != "off": + # The primary attempt may have accrued spend since initial routing. + # Retry admission uses the durable ledger for the same incarnation. + budget_session = await self.manager.get_session(self.turn.session_key) + if ( + budget_session is None + or (self.session_id is not None and budget_session.session_id != self.session_id) + or (self.epoch is not None and budget_session.epoch != self.epoch) + ): + raise RuntimeError("Quality retry budget session ownership changed") + budget_session = ( + await self.manager.storage.reconcile_session_usage_totals_from_ledger( + session_key=self.turn.session_key, + expected_session_id=budget_session.session_id, + expected_epoch=budget_session.epoch, + ) + or budget_session + ) + for field in ("billed_cost_usd", "total_cost_usd", "estimated_cost_usd"): + candidate_metadata[f"session_{field}"] = float( + getattr(budget_session, field) or 0.0 + ) + candidate_metadata["session_cost_source"] = str(budget_session.cost_source or "") + budget = common._budget_gate_input_for_turn( + candidate_turn, + router_cfg, + routed_model=candidate_turn.model, + material_tokens=max( + 1, + len( + json.dumps( + [message.model_dump(mode="json") for message in messages], + ensure_ascii=False, + default=str, + ) + ) + // 3, + ), + ) + if budget_gate(target, valid_tiers=list(router_cfg.tiers), budget=budget).tier != target: + return None + try: + # The original TurnContext predates request-window compaction. Only + # capability facts survive here; prove capacity from the final payload below. + validate_model_route_capabilities( + candidate_turn, tier=target, thinking_mode=thinking_mode, + ) + except (RuntimeError, ValueError): + return None + candidate_selector = self.selector.clone() + from opensquilla.engine.selector_override import ( + apply_model_override, + cross_provider_tier_config, + ) + + common._flag_tier_provider_mismatch( + candidate_turn, router_cfg.tiers, target, routing_applied=True, force_direct=True + ) + candidate_provider = apply_model_override( + candidate_selector, + candidate_turn.model, + turn_metadata=candidate_metadata, + realign_routed_model=False, + strict_router_fallback_chain=[], + tier_provider_config=cross_provider_tier_config( + self.turn.config, + candidate_metadata, + candidate_turn.model, + active_provider_id=candidate_selector.active_provider_id, + session_key=self.turn.session_key, + ), + ) + deployment = candidate_selector.current_config + model = str(deployment.model) + provider_id = str(deployment.provider) + if model != tier_cfg["model"] or provider_id != ( + tier_cfg.get("provider") or self.turn.config.llm.provider + ): + # A credential/continuity refusal may leave the selector on its + # original deployment. That is not admission of the requested tier. + return None + catalog = shared_catalog() + caps = catalog.resolve_deployment_capabilities( + model, + provider=provider_id, + api_key=deployment.api_key, + base_url=deployment.base_url, + ) + llm = self.turn.config.llm + window, window_source = resolve_effective_context_window( + catalog, + model, + provider=provider_id, + global_override=llm.context_window_tokens, + ) + maximum, _ = catalog.resolve_max_tokens_with_source( + model, + user_override=llm.max_tokens, + provider=provider_id, + ) + if window_source not in {"catalog", "config", "override"}: + return None + # Retain the selected authority's limits without reusing the old + # transcript estimate that may have been compacted for this request. + deployment_limits = catalog.resolve_deployment_limits( + model, provider=provider_id, api_key=deployment.api_key, + base_url=deployment.base_url, proxy=deployment.proxy, + logical_max_tokens_override=llm.max_tokens, + ) + window = min(window, deployment_limits.context_window) + if deployment_limits.max_output_tokens_known: + maximum = min(maximum, deployment_limits.max_output_tokens) + vision_support = catalog.resolve_deployment_vision_support( + model, + provider=provider_id, + api_key=deployment.api_key, + base_url=deployment.base_url, + ) + proof_budget = ContextBudgetGovernor.from_values( + context_window_tokens=window, + max_output_tokens=maximum, + thinking_budget_tokens=thinking_budget, + context_overflow_threshold=( + context_overflow_threshold if context_overflow_threshold is not None + else AgentConfig().context_overflow_threshold + ), + provider_request_proof_max_chars=chat_config.provider_request_max_chars_explicit_cap, + ).snapshot().provider_request_max_chars + bound = chat_config.model_copy( + update={ + "max_tokens": maximum, + "provider_context_window_tokens": window if window_source != "default" else 0, + "provider_request_max_chars": proof_budget, + "model_capabilities": caps, + "model_vision_support": vision_support, + "thinking": thinking_enabled, + "thinking_level": thinking_level, + "thinking_budget_tokens": thinking_budget, + "thinking_budget_explicit": ( + automatic_thinking or chat_config.thinking_budget_explicit + ), + "physical_attempt_limit": 1, + } + ) + from opensquilla.provider.execution_identity import rebind_execution_identity + + rebound = rebind_execution_identity(bound, provider=provider_id, model=model) + assert rebound is not None + bound = rebound + if prepare_request is not None: + messages, bound = prepare_request( + candidate_provider, candidate_selector, messages, tools, bound, + ) + if validate_provider_chat_admission(candidate_provider, messages, bound) is not None: + return None + projection = project_provider_final_request(candidate_provider, messages, tools, bound) + if projection is None or not projection.fits: + return None + update = { + "tier": target, + "model": model, + "provider": provider_id, + "thinking_mode": thinking_mode, + "prompt_policy": prompt_policy, + "thinking_level": level, + } + committed = await self.manager.advance_model_router_quality_retry( + self.turn.session_key, + self.input_message_id, + expected_session_id=self.session_id, + expected_session_epoch=self.epoch, + expected_version=int(state["version"]), + target_tier=target, + reason=reason, + scope_id=scope_id, + execution_update=update, + ) + if committed is None: + return None + # No request is sent before the state CAS. An activation failure remains + # an explicit failure; it must never dispatch the previous deployment. + activate_prepared(candidate_provider, deployment) + self.turn.model = model + candidate_metadata["routing_extra"] = { + **metadata.get("routing_extra", {}), + "final_tier": target, + "final_route_class": common.route_class_for_tier(target), + "thinking_mode": thinking_mode, + "prompt_policy": prompt_policy, + } + for key in tuple(metadata): + if key.startswith("routed_provider_"): + del metadata[key] + metadata.update(candidate_metadata) + metadata.update( + applied_model=model, + prompt_policy=prompt_policy, + thinking_source="model_tier_policy" if automatic_thinking else "config", + router_model_decision=committed, + requested_provider=provider_id, + requested_model=model, + resolved_model=model, + ) + metadata.pop("executed_provider", None) + metadata.pop("executed_model", None) + for key in ("savings_pct", "savings_max_price_per_m", "savings_routed_price_per_m"): + metadata[key] = 0.0 + return { + "model_id": model, + "provider_id": provider_id, + "max_tokens": bound.max_tokens, + "context_window_tokens": window, + "context_window_known": window_source != "default", + "model_capabilities": caps, + "model_vision_support": vision_support, + "thinking": (thinking_level or True) if thinking_enabled else False, + "thinking_budget_tokens": ( + bound.thinking_budget_tokens if bound.thinking_budget_explicit else -1 + ), + } diff --git a/src/opensquilla/engine/pipeline.py b/src/opensquilla/engine/pipeline.py index e4abd0ec0b..3be8d6eac2 100644 --- a/src/opensquilla/engine/pipeline.py +++ b/src/opensquilla/engine/pipeline.py @@ -61,8 +61,10 @@ def semantic_message(self) -> str: return self.raw_message if self.raw_message is not None else self.message -async def run_pipeline(ctx: TurnContext, steps: list[TurnStep]) -> TurnContext: - """Execute pipeline steps in order. Failures are logged and skipped (fail-open). +async def run_pipeline( + ctx: TurnContext, steps: list[TurnStep], *, required_steps: tuple[str, ...] = (), +) -> TurnContext: + """Execute steps in order; required step failures stop the pipeline. For each step we append one :class:`PipelineStepRecord` to ``ctx.metadata["pipeline_steps"]``: @@ -99,6 +101,8 @@ async def run_pipeline(ctx: TurnContext, steps: list[TurnStep]) -> TurnContext: fallback_reason=str(exc), ) ) + if step_name in required_steps: + raise continue applied = bool(ctx.metadata.get(f"{step_name}__applied", True)) diff --git a/src/opensquilla/engine/routing/model_policy.py b/src/opensquilla/engine/routing/model_policy.py new file mode 100644 index 0000000000..3bc61309df --- /dev/null +++ b/src/opensquilla/engine/routing/model_policy.py @@ -0,0 +1,596 @@ +"""Intent-led routing matching the frozen ``four-tier-intent-policy.v1``. + +Classification owns the tier: continue retains the last committed tier, redo +advances one tier, and new_task uses the tier head. Controller rules only +derive thinking/prompt heads. Provider preflight and durable admission belong +to the caller, not this pure transition. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import re +import uuid +from collections.abc import Callable, Mapping +from dataclasses import asdict, dataclass, field +from functools import lru_cache +from typing import Any + +import yaml + +from opensquilla.squilla_router.controller import ( + compute_difficulty, + compute_margin, + derive_prompt_policy, + normalize_decisions, + select_localized_prompt_hint, +) +from opensquilla.squilla_router.v4_phase3 import default_bundle_dir + +POLICY_VERSION = "four-tier-intent-policy.v1" +STATE_SCHEMA_VERSION = "model-tier-state.v1" +DECISION_SCHEMA_VERSION = "model-tier-decision.v1" +INTENTS = ("continue", "redo", "new_task") +TIERS = ("c0", "c1", "c2", "c3") +_NEW_CONTROLS = frozenset({"new", "new_task", "new-task", "create_task", "create-task"}) +_REDO_CONTROLS = frozenset({"redo", "regenerate", "retry_response", "retry-response"}) +_RULE_SECTIONS = ("flag_rules", "context_rules", "thinking_mode_rules", "prompt_policies") +_TRIVIAL_ACKS = frozenset( + {"thanks", "thank you", "ok", "okay", "yes", "no", "收到", "好的", "谢谢", "是的", "不用了"} +) +_CODE_BLOCK_RE = re.compile(r"\x60{3}[\s\S]*?\x60{3}") +_LOG_BLOCK_RE = re.compile( + r"(\d{4}[-/]\d{2}[-/]\d{2}[\sT]\d{2}:\d{2}.*\n){3,}" + r"|(^\[?(INFO|WARN|ERROR|DEBUG)\]?\s.*\n){3,}", + re.MULTILINE, +) +_FILE_PATH_RE = re.compile(r"""(?:^|[\s"'\x60(])([a-zA-Z_][\w.-]*/[\w./-]+\.[\w]+)""", re.MULTILINE) + + +def _hash(value: Any) -> str: + return hashlib.sha256( + json.dumps( + value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + ).hexdigest() + + +@lru_cache(maxsize=1) +def _rules() -> dict[str, Any]: + # These four sections and controller.py are identical to the frozen source; + # the bundle's deployment registry and tier-changing thresholds are unused. + data = yaml.safe_load( + (default_bundle_dir() / "router.runtime.yaml").read_text(encoding="utf-8") + ) + return {section: copy.deepcopy(data.get(section, {})) for section in _RULE_SECTIONS} + + +def _int(value: Any, default: int = 0) -> int: + return ( + value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else default + ) + + +def _tier(value: Any) -> str | None: + if isinstance(value, str): + lowered = value.lower() + if lowered in TIERS: + return lowered + if value in {"R0", "R1", "R2", "R3"}: + return f"c{value[1]}" + return None + + +def _flags(text: str, rules: Mapping[str, Any], context_tokens: int) -> dict[str, bool]: + """Exact pure flag projection from frozen four_tier_squilla_control.py.""" + configured = rules.get("flag_rules", {}) + lowered = text.lower() + + def keywords(values: Any) -> bool: + return any(str(value).lower() in lowered for value in values) + + risk = configured.get("high_risk", {}) + debug = configured.get("debug", {}) + long = configured.get("long_context", {}) + return { + "high_risk": keywords(risk.get("keywords_zh", [])) or keywords(risk.get("keywords_en", [])), + "debug": keywords(debug.get("keywords", [])) + or any(re.search(pattern, text) for pattern in debug.get("patterns", [])), + "repo_arch": keywords(configured.get("repo_arch", {}).get("keywords", [])), + "strict_format": keywords(configured.get("strict_format", {}).get("keywords", [])), + "long_context": ( + len(text) >= int(long.get("char_threshold", 6_000)) + or sum(len(match.group()) for match in _CODE_BLOCK_RE.finditer(text)) + >= int(long.get("code_block_threshold", 1_500)) + or sum(len(match.group()) for match in _LOG_BLOCK_RE.finditer(text)) + >= int(long.get("log_block_threshold", 1_500)) + or len(_FILE_PATH_RE.findall(text)) >= int(long.get("file_ref_threshold", 2)) + or context_tokens + > int(rules.get("context_rules", {}).get("heavy_context_tokens", 2_000)) + ), + } + + +def _controller( + tier: str, + vector: list[float] | None, + flags: Mapping[str, bool], + rules: Mapping[str, Any], + text: str, +) -> tuple[str, str]: + margin = compute_margin(vector) if vector is not None else 0.0 + if tier == "c3": + thinking = "T3" + else: + thinking_rules = rules.get("thinking_mode_rules", {}) + deep = thinking_rules.get("T3", {}) + minimum = _tier(deep.get("min_class", "R2")) or "c2" + if TIERS.index(tier) >= TIERS.index(minimum) and any( + flags.get(name, False) + for name in deep.get("flags", ["debug", "long_context", "high_risk"]) + ): + thinking = "T3" + else: + thinking = "T2" + for mode, default_class, default_margin in (("T0", "R0", 0.5), ("T1", "R1", 0.4)): + rule = thinking_rules.get(mode, {}) + maximum = _tier(rule.get("max_class", default_class)) or "c0" + if TIERS.index(tier) <= TIERS.index(maximum) and margin >= float( + rule.get("min_margin", default_margin) + ): + thinking = mode + break + conditions = rules.get("prompt_policies", {}).get("P0", {}).get("conditions", {}) + if vector is not None: + prompt = derive_prompt_policy( + vector, + dict(flags), + max_difficulty=float(conditions.get("max_difficulty", 0.8)), + min_margin=float(conditions.get("min_margin", 0.4)), + ) + else: + prompt = ( + "P2" + if any( + flags.get(name) for name in ("high_risk", "long_context", "debug", "strict_format") + ) + else "P1" + ) + ack = text.strip().lower().strip(" \t\r\n.!?。!?,,;;::") + if tier == "c0" and ack in _TRIVIAL_ACKS: + return "T0", "P0" + return normalize_decisions(thinking, prompt) + + +@dataclass(frozen=True) +class ModelPolicyState: + tier: str + task_id: str = field(default_factory=lambda: uuid.uuid4().hex) + turn_count: int = 1 + version: int = 1 + task_start_input_message_id: str | None = None + generation_start_input_message_id: str | None = None + + def __post_init__(self) -> None: + if self.tier not in TIERS or not isinstance(self.task_id, str) or not self.task_id.strip(): + raise ValueError("invalid model routing state identity or tier") + if any(type(value) is not int or value < 1 for value in (self.turn_count, self.version)): + raise ValueError("invalid model routing state counters") + for value in (self.task_start_input_message_id, self.generation_start_input_message_id): + if value is not None and (not isinstance(value, str) or not value.strip()): + raise ValueError("invalid model routing state boundary") + + def to_dict(self) -> dict[str, Any]: + return { + "schema_version": STATE_SCHEMA_VERSION, + "policy_version": POLICY_VERSION, + **asdict(self), + } + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> ModelPolicyState: + data = dict(value) + if ( + data.pop("schema_version", None) != STATE_SCHEMA_VERSION + or data.pop("policy_version", None) != POLICY_VERSION + ): + raise ValueError("incompatible model routing state") + return cls(**data) + + +def _probability_winner( + probabilities: Mapping[str, float], + labels: tuple[str, ...], +) -> tuple[str, float]: + """Validate one classifier boundary and apply its fixed-order argmax.""" + if ( + not isinstance(probabilities, Mapping) + or set(probabilities) != set(labels) + or any( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + or not 0 <= value <= 1 + for value in probabilities.values() + ) + or not math.isclose(sum(probabilities.values()), 1.0, rel_tol=1e-6, abs_tol=1e-6) + ): + raise ValueError("invalid classifier probability evidence") + winner = max(labels, key=probabilities.__getitem__) + return winner, probabilities[winner] + + +@dataclass(frozen=True) +class ModelPolicyAudit: + source: str + run_status: str + final: str + reason: str + prediction: str | None = None + probabilities: Mapping[str, float] | None = None + confidence: float | None = None + + def __post_init__(self) -> None: + if self.source not in { + "classifier", + "rule", + "fallback", + "not_run", + } or self.run_status not in {"ran", "error", "not_run"}: + raise ValueError("invalid model routing classifier audit") + if self.run_status == "ran": + labels = INTENTS if self.final in INTENTS else TIERS + if self.source != "classifier" or not isinstance(self.probabilities, Mapping): + raise ValueError("classifier run is missing its probabilities") + winner, confidence = _probability_winner(self.probabilities, labels) + if ( + self.prediction != winner + or self.final != winner + or isinstance(self.confidence, bool) + or self.confidence != confidence + ): + raise ValueError("classifier audit must preserve the fixed-order argmax") + elif any( + value is not None for value in (self.prediction, self.probabilities, self.confidence) + ): + raise ValueError("skipped or failed classifier cannot claim prediction evidence") + elif self.run_status == "error" and self.source != "fallback": + raise ValueError("classifier error requires an audited fallback") + + def to_dict(self) -> dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class ModelPolicyDecision: + intent: str + tier: str + raw_tier: str + intent_audit: ModelPolicyAudit + tier_audit: ModelPolicyAudit + next_state: ModelPolicyState + context_reset: bool + thinking_mode: str + prompt_policy: str + metadata: Mapping[str, Any] + + def __post_init__(self) -> None: + if self.intent not in INTENTS or self.tier not in TIERS or self.raw_tier not in TIERS: + raise ValueError("invalid model routing decision labels") + if self.intent_audit.final != self.intent or self.tier_audit.final != self.raw_tier: + raise ValueError("model routing decision disagrees with classifier audits") + if self.next_state.tier != self.tier or type(self.context_reset) is not bool: + raise ValueError("model routing decision disagrees with next state") + if self.thinking_mode not in {"T0", "T1", "T2", "T3"} or self.prompt_policy not in { + "P0", + "P1", + "P2", + }: + raise ValueError("invalid model routing controller heads") + previous = self.metadata.get("previous_tier") + if self.intent in {"continue", "redo"}: + if ( + previous not in TIERS + or self.tier_audit.run_status != "not_run" + or self.tier_audit.source != "not_run" + ): + raise ValueError("model reuse must skip tier classification") + expected = TIERS[min(TIERS.index(previous) + (self.intent == "redo"), 3)] + if self.raw_tier != expected: + raise ValueError("model reuse conflicts with previous tier and intent") + elif self.tier_audit.run_status not in {"ran", "error"}: + raise ValueError("new task must attempt tier classification") + held = self.metadata.get("held_tier") + if held is not None and held not in TIERS: + raise ValueError("invalid manual hold evidence") + if self.tier != (held or self.raw_tier) or self.metadata.get( + "router_control_hold_applied" + ) is not (held is not None): + raise ValueError("final model tier conflicts with the explicit hold") + reset = self.intent == "new_task" and self.intent_audit.source == "rule" + if self.context_reset != reset: + raise ValueError("only explicit new-task control can reset context") + if ( + self.metadata.get("quality_escalation_satisfied_by_prior_policy") is not True + or self.metadata.get("quality_escalation_prior_policy_reason") + != "model_tier_policy_resolved_explicit_correction" + ): + raise ValueError("model decision requires the explicit-correction recovery guard") + + def to_dict(self) -> dict[str, Any]: + data = { + "schema_version": DECISION_SCHEMA_VERSION, + "policy_version": POLICY_VERSION, + **asdict(self), + } + data["next_state"] = self.next_state.to_dict() + data["trace_hash"] = _hash(data) + return data + + @classmethod + def from_dict(cls, value: Mapping[str, Any]) -> ModelPolicyDecision: + data = copy.deepcopy(dict(value)) + digest = data.pop("trace_hash", None) + if digest != _hash(data): + raise ValueError("model routing decision checksum mismatch") + if ( + data.pop("schema_version", None) != DECISION_SCHEMA_VERSION + or data.pop("policy_version", None) != POLICY_VERSION + ): + raise ValueError("incompatible model routing decision") + data["next_state"] = ModelPolicyState.from_dict(data["next_state"]) + data["intent_audit"] = ModelPolicyAudit(**data["intent_audit"]) + data["tier_audit"] = ModelPolicyAudit(**data["tier_audit"]) + return cls(**data) + + +def _read(value: Any, field_name: str, default: Any = None) -> Any: + return ( + value.get(field_name, default) + if isinstance(value, Mapping) + else getattr(value, field_name, default) + ) + + +def _prediction_audit(result: Any, *, head: str, fallback: str) -> ModelPolicyAudit: + labels = INTENTS if head == "intent" else TIERS + raw = _read(result, f"{head}_probabilities") + probabilities: dict[str, float] = {} + try: + if not isinstance(raw, Mapping): + raise ValueError("missing probabilities") + for label, number in raw.items(): + name = str(label).lower() + if name in probabilities: + raise ValueError("invalid probability") + probabilities[name] = number + winner, confidence = _probability_winner(probabilities, labels) + probabilities = {name: float(number) for name, number in probabilities.items()} + confidence = float(confidence) + reported = _read(result, f"{head}_decision") + if reported is not None and probabilities.get(str(reported).lower()) != confidence: + raise ValueError("reported decision is not an argmax") + reported_confidence = _read(result, f"{head}_confidence", confidence) + if ( + isinstance(reported_confidence, bool) + or not isinstance(reported_confidence, (int, float)) + or not math.isclose(float(reported_confidence), confidence, rel_tol=1e-6, abs_tol=1e-6) + ): + raise ValueError("reported confidence differs from selected probability") + except (TypeError, ValueError, OverflowError): + return ModelPolicyAudit("fallback", "error", fallback, "invalid_classifier_result") + return ModelPolicyAudit( + "classifier", "ran", winner, "classifier_selected", winner, probabilities, confidence + ) + + +def quality_retry_tier( + current_tier: str, + reason: str, + remaining_budget: int, + *, + already_used: bool = False, + held_tier: str | None = None, +) -> str | None: + """Authorize only a bounded concrete-failure remedy; caller reserves budget. + + Preflight, current spend limits and durable CAS must pass before activation. + Transport failures are retried separately. Explicit correction is already + resolved by the intent policy and must never cause a second upgrade. + """ + if current_tier not in TIERS: + raise ValueError("invalid current tier") + if ( + reason not in {"validation_failure", "no_progress"} + or _int(remaining_budget) < 1 + or already_used + or held_tier is not None + or current_tier == "c3" + ): + return None + return TIERS[TIERS.index(current_tier) + 1] + + +def decide_model_tier( + router_input: Mapping[str, Any], + state: ModelPolicyState | None, + classifier: Any, + explicit_control: str | None = None, + held_tier: str | None = None, + default_tier: str = "c1", + input_message_id: str | None = None, + control_context: Mapping[str, Any] | None = None, + *, + task_id_factory: Callable[[], str] | None = None, +) -> ModelPolicyDecision: + if default_tier not in TIERS or (held_tier is not None and held_tier not in TIERS): + raise ValueError("invalid model routing default or manual hold") + canonical = copy.deepcopy(dict(router_input)) + if not isinstance(canonical.get("current_request"), str): + raise ValueError("model routing requires current_request text") + canonical["history_user"] = list(canonical.get("history_user", []))[-4:] + canonical["route_history"] = list(canonical.get("route_history", []))[-5:] + canonical["active_route_tier"] = state.tier.upper() if state is not None else None + event = str(explicit_control or "").strip().casefold() + explicit_intent = ( + "new_task" if event in _NEW_CONTROLS else "redo" if event in _REDO_CONTROLS else None + ) + result: Any = None + inference_error: Exception | None = None + inference_ran = False + + def predict(head: str, fallback: str) -> ModelPolicyAudit: + nonlocal result, inference_error, inference_ran + if not inference_ran: + inference_ran = True + try: + result = classifier.predict(copy.deepcopy(canonical)) + except Exception as exc: + if getattr(exc, "fail_closed", False) is True: + raise + inference_error = exc + if inference_error is not None: + return ModelPolicyAudit( + "fallback", + "error", + fallback, + f"classifier_error:{type(inference_error).__name__}", + ) + return _prediction_audit(result, head=head, fallback=fallback) + + if explicit_intent == "redo" and state is None: + intent_audit = ModelPolicyAudit( + "fallback", "not_run", "new_task", "redo_without_active_task" + ) + elif explicit_intent is not None: + intent_audit = ModelPolicyAudit( + "rule", "not_run", explicit_intent, f"explicit_{explicit_intent}_control" + ) + elif state is None: + intent_audit = ModelPolicyAudit("fallback", "not_run", "new_task", "no_active_task") + else: + intent_audit = predict("intent", "continue") + intent = intent_audit.final + if state is not None and intent in {"continue", "redo"}: + raw_tier = TIERS[min(TIERS.index(state.tier) + (intent == "redo"), 3)] + tier_audit = ModelPolicyAudit( + "not_run", + "not_run", + raw_tier, + "model_continue_keeps_previous_tier" + if intent == "continue" + else "model_redo_upgrades_previous_tier", + ) + else: + tier_audit = predict("tier", state.tier if state is not None else default_tier) + raw_tier = tier_audit.final + final_tier = held_tier or raw_tier + reset = intent_audit.source == "rule" and intent == "new_task" + if intent == "new_task": + task_id = task_id_factory() if task_id_factory else uuid.uuid4().hex + turn_count = 1 + task_start_input_message_id = input_message_id + else: + assert state is not None # Reuse is only selected for an active task above. + task_id = state.task_id + turn_count = state.turn_count + 1 + task_start_input_message_id = state.task_start_input_message_id + next_state = ModelPolicyState( + tier=final_tier, + task_id=task_id, + turn_count=turn_count, + version=state.version + 1 if state is not None else 1, + task_start_input_message_id=task_start_input_message_id, + generation_start_input_message_id=input_message_id + if reset + else state.generation_start_input_message_id + if state is not None + else None, + ) + context = dict(control_context if control_context is not None else canonical.get("context", {})) + message = canonical["current_request"] + flags_text = context.get("flags_text_override") + flags_text = flags_text if isinstance(flags_text, str) else message + rules = _rules() + flags = _flags(flags_text, rules, _int(context.get("context_tokens_est"))) + raw_vector = ( + [tier_audit.probabilities[tier] for tier in TIERS] + if tier_audit.probabilities is not None + else None + ) + mass = sum(raw_vector) if raw_vector is not None else None + vector = ( + [value / mass for value in raw_vector] if raw_vector is not None and mass else raw_vector + ) + thinking, prompt = _controller(final_tier, vector, flags, rules, flags_text) + metadata = { + "tier_policy": "model", + "tier_policy_version": POLICY_VERSION, + "raw_base_tier": raw_tier, + "final_tier": final_tier, + "previous_tier": state.tier if state is not None else None, + "raw_probabilities": dict(tier_audit.probabilities) + if tier_audit.probabilities is not None + else None, + "raw_confidence": tier_audit.confidence, + "confidence_filter_enabled": False, + "probability_margin_filter_enabled": False, + "aux_head_available": False, + "aux_downgrade_applied": False, + "flags": flags, + "runtime_rules_sha256": _hash(rules), + "margin": compute_margin(vector) if vector is not None else None, + "difficulty_score": compute_difficulty(vector) if vector is not None else None, + "thinking_mode": thinking, + "prompt_policy": prompt, + "prompt_hint": select_localized_prompt_hint( + rules.get("prompt_policies", {}).get(prompt, {}), message + ) + if prompt != "P2" + else None, + "router_control_hold_applied": held_tier is not None, + "held_tier": held_tier, + "quality_escalation_used": False, + "quality_escalation_satisfied_by_prior_policy": True, + "quality_escalation_prior_policy_reason": ( + "model_tier_policy_resolved_explicit_correction" + ), + "classifier_input_hash": _hash(canonical), + } + return ModelPolicyDecision( + intent, + final_tier, + raw_tier, + intent_audit, + tier_audit, + next_state, + reset, + thinking, + prompt, + metadata, + ) + + +def quality_retry_controls( + decision: ModelPolicyDecision, + target_tier: str, +) -> tuple[str, str]: + """Reconcile the original heads exactly as the frozen execution retry. + + Retry controller policy promotes the prior heads, without reclassifying or + rerunning text flags. + """ + from opensquilla.engine.routing.policy import reconcile_controller_with_final_tier + + if target_tier not in TIERS or TIERS.index(target_tier) != TIERS.index(decision.tier) + 1: + raise ValueError("quality recovery must advance exactly one tier") + thinking, prompt = reconcile_controller_with_final_tier( + decision.thinking_mode, + decision.prompt_policy, + {"base_tier": decision.tier, "final_tier": target_tier}, + ) + assert thinking is not None and prompt is not None + return thinking, prompt diff --git a/src/opensquilla/engine/runtime.py b/src/opensquilla/engine/runtime.py index b9ac503418..bac02fd9db 100644 --- a/src/opensquilla/engine/runtime.py +++ b/src/opensquilla/engine/runtime.py @@ -2305,10 +2305,15 @@ def __init__( health_ledger: ProviderHealthLedger | None = None, image_routing_config: Any | None = None, image_routing_session_key: str = "", + coordinate_retries: bool = False, ) -> None: self._provider = provider self._selector = selector self._turn_metadata = turn_metadata + self.coordinates_physical_retries = coordinate_retries or ( + (turn_metadata or {}).get("router_tier_policy") == "model" + and (turn_metadata or {}).get("routing_applied") is True + ) self._image_routing_config = image_routing_config self._image_routing_session_key = image_routing_session_key self._image_continuation_rebound = False @@ -2321,6 +2326,8 @@ def __init__( self._pending_fallback_hops = 0 self._last_executed_model = "" self._last_request_had_tools = False + self._last_model_chat_config: ChatConfig | None = None + self._last_model_tools: Any = None self._retry_policy = FallbackPolicy() self._image_marker_deployment: _FallbackDeploymentIdentity | None = None self._image_marker_state = ImageMarkerState.NOT_ANALYZED @@ -2357,6 +2364,54 @@ def stream_recovery_request(self) -> StreamRecoveryRequest | None: def configure_retry_policy(self, policy: FallbackPolicy) -> None: self._retry_policy = policy + async def prepare_quality_retry( + self, *, reason: str, scope_id: str, remaining_budget: int, messages: list[Any], + chat_config: ChatConfig | None = None, tools: Any = None, + context_overflow_threshold: float | None = None, + ) -> dict[str, Any] | None: + callback = (self._turn_metadata or {}).get("router_model_quality_retry_callback") + if not callable(callback): + return None + + def prepare_request( + provider: Any, selector: Any, request: list[Any], request_tools: Any, + config: ChatConfig, + ) -> tuple[list[Any], ChatConfig]: + # Preview the same physical-leg transformations used by _chat, with + # isolated selector/metadata/cache state until durable admission. + preview = object.__new__(type(self)) + preview.__dict__ = self.__dict__.copy() + preview._provider = provider + preview._selector = selector + preview._turn_metadata = dict(self._turn_metadata or {}) + preview._fallback_limits = self._fallback_limits.copy() + preview._fallback_deployment_limits = self._fallback_deployment_limits.copy() + preview._fallback_deployment_capabilities = ( + self._fallback_deployment_capabilities.copy() + ) + preview._fallback_deployment_vision_support = ( + self._fallback_deployment_vision_support.copy() + ) + preview._last_request_had_tools = bool(request_tools) + bound = preview.compaction_chat_config(config) + return preview._project_image_messages_for_active_leg( + request, bound, stage="primary", + ), bound + + return cast("dict[str, Any] | None", await callback( + reason=reason, scope_id=scope_id, remaining_budget=remaining_budget, messages=messages, + chat_config=chat_config if chat_config is not None else self._last_model_chat_config, + tools=tools if chat_config is not None else self._last_model_tools, + activate_prepared=self.activate_prepared_deployment, + prepare_request=prepare_request, + context_overflow_threshold=context_overflow_threshold, + )) + + def activate_prepared_deployment(self, provider: Any, deployment: ProviderConfig) -> None: + """Install the exact retry leg that passed preflight before its durable CAS.""" + self._selector.override_provider_config(deployment, preserve_existing_tail=False) + self._provider = provider + async def _retry_provider_stream( self, stream_factory: Callable[[], AsyncGenerator[Any, None]], @@ -2376,7 +2431,7 @@ def reset_attempt() -> None: provider=provider, model=model, config=config, content_started=content_started, reset_attempt=reset_attempt, - max_retries=self._retry_policy.max_retries, + max_retries=0 if self.coordinates_physical_retries else self._retry_policy.max_retries, base_ms=self._retry_policy.base_backoff_ms, max_ms=self._retry_policy.max_backoff_ms, # Preserve selector semantics: overload/transport may choose another @@ -2432,11 +2487,25 @@ def clone_config(cfg: ProviderConfig) -> ProviderConfig: frozen_selector.override_model(model) frozen_provider = frozen_selector.resolve() metadata_copy = dict(self._turn_metadata) if isinstance(self._turn_metadata, dict) else None + if self.coordinates_physical_retries: + # Child execution has independent routing state and accounting, but + # failures still belong to the physical credential pool it inherited. + source_metadata = metadata_copy or {} + metadata_copy = { + key: copy.deepcopy(source_metadata[key]) + for key in ( + "credential_pool", "routed_provider_applied", + "session_credential_pool", "session_provider_applied", + "routed_provider_fallback_reason", + ) + if key in source_metadata + } return _SelectorFallbackProvider( frozen_provider, frozen_selector, turn_metadata=metadata_copy, health_ledger=self._health_ledger, + coordinate_retries=self.coordinates_physical_retries, ) @property @@ -2821,13 +2890,27 @@ async def prepare_image_continuation( if ( not getattr(router_config, "enabled", False) or getattr(router_config, "rollout_phase", "observe") == "observe" - or not callable(self._image_catalog_lookup) or not _count_image_blocks(messages) ): return None if self.active_model_vision_support(config) == "supported": return cast(ChatConfig, self._config_for_active_leg(config)) + metadata = self._turn_metadata or {} + if ( + metadata.get("router_tier_policy") == "model" + and metadata.get("routing_applied") is True + ): + from opensquilla.engine.steps.model_router import ModelRouteError + + # Tool output continues the admitted task; it must not classify an + # empty user message or substitute a different tier for image support. + raise ModelRouteError( + f"Selected tier {metadata.get('routed_tier')} has no proven image support" + ) + if not callable(self._image_catalog_lookup): + return None + from opensquilla.engine.history import ( HistoryReplayProjection, project_history_replay_capacity, @@ -3115,6 +3198,8 @@ def _config_for_active_leg(self, config: Any) -> Any: provider_id, model = self._active_deployment() config = rebind_execution_identity(config, provider=provider_id, model=model) + if self.coordinates_physical_retries and isinstance(config, ChatConfig): + config = config.model_copy(update={"physical_attempt_limit": 1}) if not self._used_fallback and not self._image_continuation_rebound: return config updates: dict[str, Any] = {} @@ -3804,6 +3889,8 @@ async def _chat( execution_context: TurnExecutionContext | None = None, ) -> AsyncIterator[Any]: self._last_request_had_tools = bool(tools) + self._last_model_chat_config = config if isinstance(config, ChatConfig) else None + self._last_model_tools = tools self._stream_recovery_request = None emitted_user_visible_content = False pre_text_buffer = _SelectorPreTextBuffer() @@ -5105,6 +5192,32 @@ def _turn_config(self) -> Any: # config object in accepted_turn_config_scope(). return accepted + def _uses_model_tier_policy(self, session_key: str) -> bool: + config = getattr(self._turn_config(), "squilla_router", None) + return bool( + getattr(config, "enabled", False) + and getattr(config, "strategy", "lightgbm_model") != "legacy_v4_phase3" + and ":subagent:" not in session_key + ) + + async def _model_router_generation( + self, session_key: str, bound_user_message_id: str | None, *, + expected_session_id: str | None, expected_session_epoch: int | None, + ) -> list[Any] | None: + if not self._uses_model_tier_policy(session_key): + return None + from opensquilla.session.model_router_state import model_generation_transcript + + if self._session_manager is None: + raise RuntimeError("Model routing requires a session manager") + canonical = await self._session_manager.get_model_router_transcript( + session_key, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + return model_generation_transcript( + canonical, bound_user_message_id, include_current_staged=True, + ) + def _session_model_pin_applies(self) -> bool: """A saved single-model choice must not override a routed session turn.""" return getattr(_ACCEPTED_TURN_CONFIG.get(), "session_mode", None) not in { @@ -7638,6 +7751,26 @@ def resolve_retained_attachment( ), initial_provider_config=capacity_initial_provider_config, ) + if turn.metadata.get("router_model_capacity_retry_pending") is True: + if agent.config.metadata.get("router_model_prepared_window_admitted") is True: + # The selected deployment's exact consumer gate proved + # this request-local window, including its summary. + turn.metadata["router_model_capacity_retry_pending"] = False + else: + await self._readmit_attachment_capacity( + turn, cloned_selector, RouterHistoryReplayRequest( + exclude_last_user=history_has_persisted_user, + bound_user_message_id=bound_user_message_id, + transcript_snapshot=transcript_snapshot, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ), initial_provider_config=capacity_initial_provider_config, + ) + await self._admit_model_router_turn( + turn, bound_user_message_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) compaction_source_entries: tuple[Any, ...] | None = None compaction_source_preimage: tuple[tuple[Any, ...], ...] | None = None @@ -10252,6 +10385,7 @@ async def _run_pipeline( prev_assistant_text: str | None = None, prev_assistant_usage: dict[str, Any] | None = None, history_user_texts: list[str] | None = None, + model_router_context: dict[str, Any] | None = None, history_capacity_estimated_tokens: int = 0, history_capacity_message_count: int = 0, history_capacity_estimate_complete: bool = True, @@ -10303,6 +10437,7 @@ async def _run_pipeline( router_cfg = getattr(self._turn_config(), "squilla_router", None) router_timeout = float(getattr(router_cfg, "routing_timeout_seconds", 5.0) or 5.0) + model_policy_active = self._uses_model_tier_policy(session_key) def _copy_router_turn(turn: TurnContext) -> TurnContext: # Detach mutable per-turn facts, but retain opaque runtime services. @@ -10520,6 +10655,20 @@ def _run_router_step_sync() -> TurnContext: initial_metadata["router_prev_assistant_usage"] = dict(prev_assistant_usage) if history_user_texts: initial_metadata["router_history_user_texts"] = list(history_user_texts) + if model_policy_active: + from opensquilla.session.model_router_state import prepare_model_router_metadata + + initial_metadata.update(prepare_model_router_metadata( + model_router_context, + current_request=semantic_message if semantic_message is not None else message, + input_message_id=bound_user_message_id, + history_user=history_user_texts, + previous_answer=prev_assistant_text, + previous_usage=prev_assistant_usage, + tool_defs=tool_defs, + attachments=attachments, + channel_kind=str(getattr(tool_context, "channel_kind", "") or "").strip(), + )) initial_metadata["routing_history_capacity_estimated_tokens"] = max( 0, int(history_capacity_estimated_tokens), @@ -10627,6 +10776,8 @@ def _run_router_step_sync() -> TurnContext: from opensquilla.engine.steps.selected_skills import load_selected_skills turn = await load_selected_skills(turn, tool_context) + if model_policy_active and turn.surface_kind and turn.surface_kind != "unknown": + turn.metadata["router_model_input"]["context"]["entrypoint"] = turn.surface_kind pipeline_steps: list[TurnStep] = [resolve_model] pipeline_steps.extend( [ @@ -10642,7 +10793,25 @@ def _run_router_step_sync() -> TurnContext: apply_prompt_cache, ] ) - turn = await run_pipeline(turn, pipeline_steps) + turn = await run_pipeline( + turn, pipeline_steps, + required_steps=("apply_squilla_router",) if model_policy_active else (), + ) + if ( + turn.metadata.get("routing_applied") is True + and turn.metadata.get("router_model_decision") is not None + ): + if self._session_manager is None or not bound_user_message_id: + raise RuntimeError("Model routing requires a durable accepted user input") + record = turn.metadata["router_model_decision"] + committed = await self._session_manager.stage_model_router_decision( + session_key, bound_user_message_id, record, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + if committed.get("state") != record.get("state"): + raise RuntimeError("Model router input already has a different durable decision") + turn.metadata["router_model_decision"] = committed if router_history_replay_request is not None: history_capacity = await self._router_history_capacity_for_request( session_key, @@ -10688,6 +10857,49 @@ def _run_router_step_sync() -> TurnContext: reason="Attachment request capacity could not be established.", ) + if ( + turn.metadata.get("router_tier_policy") == "model" + and turn.metadata.get("routing_applied") is True + ): + from opensquilla.engine.model_router_quality import ModelRouterQualityRetry + from opensquilla.engine.selector_override import ( + apply_model_override, + cross_provider_tier_config, + ) + + if cloned_selector is None: + raise RuntimeError("Model routing requires a concrete provider selector") + provider = apply_model_override( + cloned_selector, turn.model, turn_metadata=turn.metadata, + realign_routed_model=False, strict_router_fallback_chain=[], + tier_provider_config=cross_provider_tier_config( + turn.config, turn.metadata, turn.model, + active_provider_id=getattr(cloned_selector, "active_provider_id", ""), + session_key=session_key, + ), + ) + tier = turn.config.squilla_router.tiers[turn.metadata["routed_tier"]] + deployment = cloned_selector.current_config + if deployment.model != tier["model"] or deployment.provider != ( + tier.get("provider") or turn.config.llm.provider + ): + from opensquilla.engine.steps.model_router import ModelRouteError + + raise ModelRouteError("Selected model-policy deployment could not be resolved") + if bound_user_message_id and self._session_manager is not None: + turn.metadata["router_model_quality_retry_callback"] = ModelRouterQualityRetry( + turn=turn, selector=cloned_selector, manager=self._session_manager, + input_message_id=bound_user_message_id, + session_id=expected_session_id, epoch=expected_session_epoch, + ) + if turn.metadata.get("router_model_capacity_retry_pending") is not True: + await self._admit_model_router_turn( + turn, bound_user_message_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + return turn, provider + # Image routing is a capability boundary, not an Ensemble activation. # This applies to the dedicated image row and to any text tier selected # as the image-capable fallback. Resolve that deployment through the @@ -11505,6 +11717,25 @@ def _entry_projector(entry: Any, entry_index: int) -> HistoryReplayEntryProjecti entry_projector=_entry_projector, ) + async def _admit_model_router_turn( + self, turn: TurnContext, input_message_id: str | None, *, + expected_session_id: str | None, expected_session_epoch: int | None, + ) -> None: + envelope = turn.metadata.get("router_model_decision") + if not isinstance(envelope, dict) or not input_message_id: + raise RuntimeError("Model admission requires a staged accepted input") + if self._session_manager is None: + raise RuntimeError("Model routing admission requires a session manager") + committed = await self._session_manager.admit_model_router_decision( + turn.session_key, input_message_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + expected_previous_version=(int(envelope["state"]["version"]) - 1) or None, + ) + if not isinstance(committed, dict) or committed.get("state_committed") is not True: + raise RuntimeError("Model routing state admission lost its version fence") + turn.metadata["router_model_decision"] = committed + async def _readmit_attachment_capacity( self, turn: TurnContext, @@ -11521,6 +11752,7 @@ async def _readmit_attachment_capacity( from opensquilla.engine.selector_override import require_current_selector_capacity from opensquilla.engine.steps.squilla_router import finalize_squilla_router_capacity + model_policy = turn.metadata.get("router_tier_policy") == "model" if request.transcript_snapshot is not None: request.transcript_snapshot.invalidate() history_capacity = await self._router_history_capacity_for_request( @@ -11528,7 +11760,8 @@ async def _readmit_attachment_capacity( request, max_history_turns=0, preserve_image_attachments=( - turn.metadata.get("image_route_reason") in {"current_turn", "history_context"} + model_policy + or turn.metadata.get("image_route_reason") in {"current_turn", "history_context"} ), reachable_provider_kinds=self._route_capacity_provider_kinds( turn, initial_provider_config=initial_provider_config, @@ -11546,10 +11779,11 @@ async def _readmit_attachment_capacity( ), }) await finalize_squilla_router_capacity(turn, retry_after_compaction=True) - require_current_selector_capacity( - selector, turn.metadata, - reason="Attachment request did not pass capacity admission after compaction.", - ) + if not model_policy: + require_current_selector_capacity( + selector, turn.metadata, + reason="Attachment request did not pass capacity admission after compaction.", + ) async def _router_history_capacity_for_request( self, @@ -11602,6 +11836,14 @@ async def _router_history_capacity_for_request( transcript = await transcript entries = list(transcript or []) + generation = await self._model_router_generation( + session_key, request.bound_user_message_id, + expected_session_id=request.expected_session_id, + expected_session_epoch=request.expected_session_epoch, + ) + model_generation_reset = generation is not None + if generation is not None: + entries = generation bound_index: int | None = None if request.bound_user_message_id is not None: for index, entry in enumerate(entries): @@ -11619,6 +11861,7 @@ async def _router_history_capacity_for_request( reachable_provider_kinds=reachable_provider_kinds, expected_session_id=request.expected_session_id, expected_session_epoch=request.expected_session_epoch, + exclude_compaction_context=model_generation_reset, ) except Exception as exc: # noqa: BLE001 - capacity admission fails closed # Never serialize the exception: storage/provider errors may echo @@ -11627,6 +11870,10 @@ async def _router_history_capacity_for_request( "turn_runner.router_capacity_projection_failed", error_type=type(exc).__name__, ) + if isinstance(exc, StaleEpochError): + return {"history_capacity_estimate_complete": False, + "history_capacity_estimated_tokens": 0, + "history_capacity_message_count": 0} return {"history_capacity_estimate_complete": False} async def _router_history_capacity_context( @@ -11642,6 +11889,7 @@ async def _router_history_capacity_context( reachable_provider_kinds: Collection[str] | None = None, expected_session_id: str | None = None, expected_session_epoch: int | None = None, + exclude_compaction_context: bool = False, ) -> dict[str, Any]: """Measure the route-specific pre-current replay projection.""" @@ -11691,6 +11939,15 @@ async def _router_history_capacity_context( replay, max_history_turns=max_history_turns, ) + if exclude_compaction_context: + return { + "history_capacity_estimated_tokens": capacity.estimated_tokens, + "history_capacity_message_count": capacity.message_count, + "history_capacity_estimate_complete": bool( + capacity.estimate_complete + and (bound_user_message_id is None or bound_index is not None) + ), + } legacy_summary_markers = list(replay.legacy_summary_markers) summaries: list[Any] = [] @@ -11857,7 +12114,10 @@ async def _router_previous_assistant_context( else {} ) get_transcript = getattr(self._session_manager, "get_transcript", None) + model_policy_active = self._uses_model_tier_policy(session_key) if not callable(get_transcript): + if model_policy_active: + raise RuntimeError("Model router requires an authoritative transcript reader") return {"history_capacity_estimate_complete": False} if include_capacity else {} try: exact_owner = _require_optional_exact_session_owner( @@ -11885,10 +12145,66 @@ async def _router_previous_assistant_context( transcript = get_transcript(session_key, **transcript_kwargs) if inspect.isawaitable(transcript): transcript = await transcript - except Exception: # noqa: BLE001 - router context must never block a turn + except Exception: # noqa: BLE001 - legacy router context remains best-effort + if model_policy_active: + raise log.debug("turn_runner.router_context_failed", session_key=session_key) return {"history_capacity_estimate_complete": False} if include_capacity else {} entries = list(transcript or []) + model_context: dict[str, Any] = {} + model_generation_reset = False + if model_policy_active: + from opensquilla.session.model_router_state import ( + build_model_router_context, + model_generation_transcript, + ) + + model_entries = await self._session_manager.get_model_router_transcript( + session_key, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + model_session_id = expected_session_id or next(( + row.session_id for row in model_entries if row.session_id + ), None) + canonical_complete = False + if model_session_id: + coverage = await self._session_manager.storage.get_canonical_transcript_coverage( + model_session_id + ) + canonical_complete = coverage.canonical_complete is True + model_context = build_model_router_context( + model_entries, + bound_user_message_id=bound_user_message_id, + exclude_last_user=exclude_last_user, + feature_entries=entries, + canonical_complete=canonical_complete, + ) + # Capacity and image facts must describe the same generation that + # _load_history will send. Keep classifier features above unchanged. + # Explicit New Task has not been staged yet at this boundary. + control = model_context.get("router_model_control_event") + router_cfg = getattr(self._turn_config(), "squilla_router", None) + if ( + getattr(router_cfg, "rollout_phase", "full") != "observe" + and not model_context.get("router_model_decision") + and control in {"new", "new_task", "new-task", "create_task", "create-task"} + ): + current = next(( + row for row in reversed(model_entries) + if row.role == "user" and ( + bound_user_message_id is None or row.message_id == bound_user_message_id + ) + ), None) + entries = [current] if current is not None else [] + model_generation_reset = True + else: + generation = model_generation_transcript( + model_entries, bound_user_message_id, include_current_staged=True, + ) + if generation is not None: + entries = generation + model_generation_reset = True # When the turn is bound to a specific user message id (queued-sends # path), exclude the bound current prompt AND every later user entry # (still-queued future prompts persisted at ingress), mirroring @@ -11921,6 +12237,7 @@ async def _router_previous_assistant_context( bound_index=bound_index, expected_session_id=expected_session_id, expected_session_epoch=expected_session_epoch, + exclude_compaction_context=model_generation_reset, ) user_texts: list[str] = [] @@ -11971,7 +12288,7 @@ async def _router_previous_assistant_context( text = text[-_ROUTER_HISTORY_USER_MAX_CHARS:] user_texts.append(text) - context: dict[str, Any] = dict(capacity_context) + context: dict[str, Any] = {**capacity_context, **model_context} if user_texts: context["history_user_texts"] = user_texts[-_ROUTER_HISTORY_USER_MAX_TURNS:] router_cfg = getattr(self._turn_config(), "squilla_router", None) @@ -12695,6 +13012,171 @@ def require_parent_time(*, terminalize: bool = False) -> None: transcript = await get_transcript(session_key, **transcript_kwargs) except KeyError: return None # session doesn't exist yet + generation = await self._model_router_generation( + session_key, bound_user_message_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + if generation is not None: + # Explicit New Task owns only a request-local generation. + # Keep the user's transcript and prior checkpoints unchanged. + if consumer_admission is not None and consumer_admission( + "", [self._entry_for_emergency_compaction(row) for row in generation], + ): + return None + if consumer_admission is None: + from opensquilla.session.compaction import ( + estimate_entries_model_replay_chars, + estimate_entry_model_replay_tokens, + ) + + if ( + sum(estimate_entry_model_replay_tokens(row) for row in generation) + <= history_window_tokens + and (history_capacity_chars is None or + estimate_entries_model_replay_chars(generation) + <= history_capacity_chars) + ): + return None + protected_suffix_count = self._protected_current_turn_suffix_count( + generation, history_has_persisted_user=history_has_persisted_user, + bound_user_message_id=bound_user_message_id, + ) + compaction_id = new_compaction_id() + self.mark_compaction_attempted_this_turn(session_key) + if ( + getattr(configured_compaction, "enabled", True) is not False + and not self._compaction_circuit_open(session_key) + ): + from opensquilla.session.compaction import ( + CompactionRequest, + compact_context, + compaction_replay_summary, + ) + + compaction_config.protected_recent_messages = max( + effective_protected_recent_messages(compaction_config), + protected_suffix_count, + ) + arm_compaction_deadline(compaction_config, operation_id=compaction_id) + notify_compaction( + session_key, source="automatic", phase="preflight", status="started", + reason="model_generation_context", + **compaction_effect_payload(status="started"), + **compaction_lifecycle_payload(compaction_id, COMPACTION_TRIGGERED_EVENT), + ) + generation_summary_started = False + previous_generation_started = compaction_config.on_summary_call_started + + def on_generation_summary_started() -> None: + nonlocal generation_summary_started + generation_summary_started = True + if previous_generation_started is not None: + previous_generation_started() + + compaction_config.on_summary_call_started = on_generation_summary_started + try: + # Summarize only this task's canonical generation. The + # resulting view is request-owned: never read a prior task's + # checkpoint or rewrite the user's durable transcript. + generation_result = await compact_context(CompactionRequest( + session_id=expected_session_id or next( + (row.session_id for row in generation), session_key, + ), + entries=[self._entry_for_emergency_compaction(row) for row in generation], + context_window_tokens=history_window_tokens, + context_window_chars=history_capacity_chars, + config=compaction_config, + previous_summary="", + summary_replay_renderer=lambda text: ( + _format_compaction_summary_context([text]) or "" + ), + consumer_admission=consumer_admission, + provider_request_correlation=provider_request_correlation, + reason="model_generation_context", + # Exact consumer admission above has already found + # pressure, including non-token provider limits. Keep + # the compactor's complete-round and protected-tail gates. + force=True, + )) + except asyncio.CancelledError: + notify_compaction( + session_key, source="automatic", phase="preflight", status="cancelled", + reason="model_generation_context", + **compaction_effect_payload(status="cancelled"), + **compaction_lifecycle_payload(compaction_id, COMPACTION_TRIGGERED_EVENT), + ) + raise + except CompactionTimeoutError: + require_parent_time(terminalize=True) + if generation_summary_started: + self._record_compaction_failure( + session_key, failure_kind="operation_timeout", + ) + generation_result = None + finally: + compaction_config.on_summary_call_started = previous_generation_started + require_parent_time(terminalize=True) + if (generation_result is not None and generation_result.removed_count > 0 + and generation_result.summary): + summary = compaction_replay_summary(generation_result) + generation_window = PreparedRequestWindow( + session_key=session_key, summary=summary, + kept_entries=tuple( + self._emergency_replay_entry(row) + for row in generation_result.kept_entries + ), + reason="model_generation_context", compaction_id=compaction_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + source_fingerprint=self._emergency_source_fingerprint(generation), + history_window_tokens=history_window_tokens, + history_capacity_chars=history_capacity_chars, + protected_recent_messages=protected_suffix_count, + preserve_historical_images=preserve_historical_images, + consumer_admission=consumer_admission, + ) + self.mark_compacted_this_turn(session_key) + self._record_compaction_success(session_key) + completed_payload = compaction_lifecycle_payload( + compaction_id, COMPACTION_TRIGGERED_EVENT, + ) + completed_payload.update(compaction_result_payload(generation_result)) + notify_compaction( + session_key, source="automatic", phase="preflight", status="completed", + reason="model_generation_context", archived_count=0, + **compaction_effect_payload( + status="completed", durability="request_scoped", + ), + **completed_payload, + ) + return generation_window + skip_reason = ( + (generation_result.skip_reason or "empty_summary") + if generation_result is not None + else "compaction_deadline_exceeded" + ) + if generation_result is not None and generation_summary_started: + self._record_compaction_failure( + session_key, failure_kind=generation_result.failure_kind or skip_reason, + ) + notify_compaction( + session_key, source="automatic", phase="preflight", status="failed", + reason=skip_reason, + **compaction_effect_payload(status="failed", reason=skip_reason), + **compaction_lifecycle_payload(compaction_id, COMPACTION_TRIGGERED_EVENT), + ) + return await self._prepare_request_window( + session_key, generation, history_window_tokens, + compaction_id=compaction_id, phase="preflight", + reason="model_generation_context", + protected_recent_messages=protected_suffix_count, + history_capacity_chars=history_capacity_chars, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + consumer_admission=consumer_admission, + preserve_historical_images=preserve_historical_images, + ) ( checkpoint_tokens, checkpoint_chars, @@ -13558,9 +14040,12 @@ async def _prepare_request_window( ) from opensquilla.session.tokenizer import estimate_tokens - context_records = await self._load_compaction_context_records( - session_key, [], expected_session_id=expected_session_id, - expected_session_epoch=expected_session_epoch, + context_records = ( + [] if reason == "model_generation_context" + else await self._load_compaction_context_records( + session_key, [], expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) ) recovered_entries = [entry for record in context_records for entry in record.source_entries] @@ -14087,13 +14572,47 @@ async def _load_history( transcript_kwargs["expected_session_epoch"] = expected_session_epoch transcript = await get_transcript(session_key, **transcript_kwargs) + generation = await self._model_router_generation( + session_key, bound_user_message_id, + expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + model_generation_reset = generation is not None + if generation is not None: + transcript = generation + if prepared_window is not None: + if prepared_window.reason != "model_generation_context": + prepared_window = None + else: + if ( + prepared_window.expected_session_id != expected_session_id + or prepared_window.expected_session_epoch != expected_session_epoch + ): + raise RuntimeError("Model generation window ownership changed") + if ( + self._emergency_source_fingerprint(generation) + != prepared_window.source_fingerprint + ): + raise RuntimeError( + "Model generation changed after request admission" + ) + if prepared_window.consumer_admission is not None and not ( + prepared_window.consumer_admission( + prepared_window.summary, + [self._entry_for_emergency_compaction(row) + for row in prepared_window.kept_entries], + ) + ): + raise RuntimeError("Model generation window lost request admission") + transcript = list(prepared_window.kept_entries) + from opensquilla.engine.history import reconstruct_messages_from_entry from opensquilla.provider.types import ContentBlockImage, ContentBlockText history: list[Message] = [] summary_markers: list[str] = [] exact_owner = expected_session_id is not None or expected_session_epoch is not None - if prepared_window is not None: + if prepared_window is not None and not model_generation_reset: override_has_owner = ( prepared_window.expected_session_id is not None or prepared_window.expected_session_epoch is not None @@ -14667,10 +15186,19 @@ async def _load_history( context_states=context_states, provider_kind=str(getattr(provider, "provider_name", "")), ) - if provider_context.messages and prepared_window is None: + if provider_context.messages and prepared_window is None and not model_generation_reset: history = provider_context.messages + history if history: agent.set_history(history) + if prepared_window is not None and prepared_window.consumer_admission is not None: + agent_metadata = getattr(agent_config, "metadata", None) + if isinstance(agent_metadata, dict): + agent_metadata["router_model_prepared_window_admitted"] = True + return _format_compaction_summary_context([prepared_window.summary]) + if model_generation_reset: + # A checkpoint can summarize turns from before explicit New Task. + # The canonical generation above is the complete authorized replay. + return None if prepared_window is not None: # Already contains the optional complete checkpoint exactly once. return _format_compaction_summary_context([prepared_window.summary]) diff --git a/src/opensquilla/engine/steps/model_router.py b/src/opensquilla/engine/steps/model_router.py new file mode 100644 index 0000000000..0017c5a037 --- /dev/null +++ b/src/opensquilla/engine/steps/model_router.py @@ -0,0 +1,329 @@ +"""Intent-led routing using the pinned pair from the model-policy experiment. + +The state machine selects a semantic tier. Physical admission validates that +exact deployment; capability, budget and cache heuristics never replace it. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import replace + +from opensquilla.engine.pipeline import TurnContext +from opensquilla.engine.routing.policy import RoutingDecision +from opensquilla.router_control import RouterControlHoldStore +from opensquilla.router_tiers import TEXT_TIERS + + +class ModelRouteError(RuntimeError): + """The selected model-policy route cannot execute faithfully.""" + + +async def apply_model_router(ctx: TurnContext) -> TurnContext: + from opensquilla.engine.routing.model_policy import ( + ModelPolicyDecision, + ModelPolicyState, + decide_model_tier, + ) + from opensquilla.engine.steps import squilla_router as common + from opensquilla.engine.steps.router_decision_record import stage_router_decision + + config = ctx.config.squilla_router + tiers = config.tiers + phase = config.rollout_phase + state_raw = ctx.metadata.get("router_model_state") + state = ModelPolicyState.from_dict(state_raw) if isinstance(state_raw, Mapping) else None + raw_envelope = ctx.metadata.get("router_model_decision") + envelope = raw_envelope if isinstance(raw_envelope, Mapping) else {} + replay = isinstance(envelope.get("decision"), Mapping) + if replay and envelope.get("status") == "staged": + # The source retains rejected/incomplete admission for idempotency; + # it never reruns classification or treats that proposed tier as live. + raise ModelRouteError("This input has a staged model decision that was not admitted") + held_tier = None + hold = None + if not replay: + store = ctx.metadata.get("router_control_hold_store") + if isinstance(store, RouterControlHoldStore): + hold = store.get_valid( + ctx.session_key, + decrement=phase != "observe", + routing_revision=ctx.metadata.get("router_control_routing_revision"), + ) + held_tier = hold.tier if hold else None + if replay: + decision = ModelPolicyDecision.from_dict(envelope["decision"]) + else: + router_input = ctx.metadata.get("router_model_input") + if not isinstance(router_input, Mapping): + from opensquilla.session.model_router_state import prepare_model_router_metadata + + ctx.metadata.update( + prepare_model_router_metadata( + ctx.metadata, + current_request=ctx.semantic_message, + input_message_id=ctx.metadata.get("router_model_input_message_id"), + history_user=ctx.metadata.get("router_history_user_texts"), + previous_answer=ctx.metadata.get("router_prev_assistant_text"), + previous_usage=ctx.metadata.get("router_prev_assistant_usage"), + tool_defs=ctx.tool_defs, + attachments=ctx.attachments, + ) + ) + router_input = ctx.metadata["router_model_input"] + router_input["active_route_tier"] = state.tier.upper() if state else None + strategy = common._get_strategy(config) + decision = decide_model_tier( + router_input, + state, + strategy, + explicit_control=ctx.metadata.get("router_model_control_event"), + held_tier=held_tier, + default_tier=config.default_tier, + input_message_id=ctx.metadata.get("router_model_input_message_id"), + control_context=ctx.metadata.get("router_model_control_context"), + ) + selected_tier = decision.tier + effective_state = decision.next_state + thinking_mode = decision.thinking_mode + prompt_policy = decision.prompt_policy + prompt_hint = decision.metadata.get("prompt_hint") + if replay and isinstance(envelope.get("execution_update"), Mapping): + update = envelope["execution_update"] + effective_state = ModelPolicyState.from_dict(envelope["state"]) + if update.get("tier") != effective_state.tier: + raise ModelRouteError("Replayed execution tier does not match committed state") + if update.get("thinking_mode") not in {"T0", "T1", "T2", "T3"} or update.get( + "prompt_policy" + ) not in {"P0", "P1", "P2"}: + raise ModelRouteError("Replayed execution control is invalid") + selected_tier = effective_state.tier + thinking_mode = update["thinking_mode"] + prompt_policy = update["prompt_policy"] + prompt_hint = None + if selected_tier not in TEXT_TIERS: + raise ModelRouteError("Model policy selected an invalid tier") + tier_config = tiers.get(selected_tier) + if not isinstance(tier_config, Mapping) or not str(tier_config.get("model") or "").strip(): + raise ModelRouteError(f"Model policy selected unconfigured tier {selected_tier}") + if replay and isinstance(envelope.get("execution_update"), Mapping): + update = envelope["execution_update"] + provider = str(tier_config.get("provider") or ctx.config.llm.provider) + if update.get("model") != tier_config["model"] or update.get("provider") != provider: + raise ModelRouteError("Replayed model deployment changed after execution") + baseline = ctx.model + routed_model = str(tier_config["model"]) + applied = phase != "observe" + if applied: + ctx.model = routed_model + source = ( + "router_control_hold" + if held_tier or decision.metadata.get("router_control_hold_applied") + else "model_tier_policy" + ) + confidence = decision.tier_audit.confidence + if confidence is None: + confidence = decision.intent_audit.confidence or 0.0 + extra = dict(decision.metadata) + extra.update( + tier_policy="model", + base_tier=decision.raw_tier, + final_tier=selected_tier, + route_class=common.route_class_for_tier(decision.raw_tier), + final_route_class=common.route_class_for_tier(selected_tier), + thinking_mode=thinking_mode, + prompt_policy=prompt_policy, + intent=decision.intent, + ) + ctx.metadata.update( + router_tier_policy="model", + routed_tier=selected_tier, + routed_model=routed_model, + routed_provider=str(tier_config.get("provider") or ctx.config.llm.provider), + routing_source=source, + routing_confidence=confidence, + baseline_model=baseline, + applied_model=ctx.model, + routing_applied=applied, + rollout_phase=phase, + routing_extra=extra, + ) + if applied: + # Execution isolation is inactive in observation mode. + ctx.metadata.update( + router_fallback_chain=[], + router_fallback_strict=True, + quality_escalation_satisfied_by_prior_policy=True, + quality_escalation_prior_policy_reason="model_tier_policy_resolved_explicit_correction", + ) + if not replay: + ctx.metadata["router_model_decision"] = { + "schema_version": 1, + "input_message_id": ctx.metadata.get("router_model_input_message_id"), + "state": effective_state.to_dict(), + "decision": decision.to_dict(), + } + if decision.context_reset: + ctx.metadata["provider_state_replay_disabled"] = "router_explicit_new_task" + if applied and decision.metadata.get("router_control_hold_applied") is True: + ctx.metadata["router_control_hold_applied"] = True + if applied and hold is not None: + ctx.metadata.update( + router_control_hold_applied=True, + router_control_action="set_hold", + router_control_target_tier=hold.tier, + router_control_target_model=hold.model, + router_control_target_provider=hold.provider, + router_control_evidence=hold.evidence, + ) + if applied: + common._apply_controller( + ctx, + config, + dict(tier_config), + thinking_mode, + prompt_policy, + prompt_hint, + phase, + ) + else: + ctx.metadata.update(thinking_mode=thinking_mode, prompt_policy=prompt_policy) + if applied and phase == "full" and config.auto_thinking: + # Preserve the policy's explicit T0 disable when automatic thinking is + # enabled. Runtime still gives an explicit llm.thinking setting priority. + ctx.metadata.update( + thinking_requested=True, + thinking_level={"T0": "off", "T1": "low", "T2": "medium", "T3": "high"}[thinking_mode], + thinking_source="model_tier_policy", + ) + ctx.metadata.update(common._compute_savings(routed_model, tiers)) + common._flag_tier_provider_mismatch( + ctx, tiers, selected_tier, routing_applied=applied, force_direct=True + ) + stage_router_decision( + ctx, + decision=RoutingDecision( + tier=selected_tier, + model=routed_model, + confidence=confidence, + source=source, + ), + ) + return ctx + + +def validate_model_route_capabilities( + ctx: TurnContext, + *, + tier: str | None = None, + thinking_mode: str | None = None, +) -> TurnContext: + """Check deployment capabilities independently of historical size estimates.""" + if ctx.metadata.get("routing_applied") is not True: + return ctx + from opensquilla.engine.steps import squilla_router as common + from opensquilla.provider.model_catalog import shared_catalog + + config = ctx.config.squilla_router + selected = tier or ctx.metadata.get("routed_tier") + if not isinstance(selected, str): + raise ModelRouteError("Selected model-policy tier is missing") + raw = config.tiers.get(selected) + if not isinstance(raw, dict): + raise ModelRouteError("Selected model-policy deployment is missing") + llm = ctx.config.llm + provider = str(raw.get("provider") or llm.provider) + if provider != str(llm.provider) and not config.cross_provider_tiers: + raise ModelRouteError( + f"Selected tier {selected} requires cross-provider routing for {provider}" + ) + model = str(raw.get("model") or "") + has_images = ( + common._attachments_include_image(ctx.attachments) + or ctx.metadata.get("image_context_has_images") is True + or ctx.metadata.get("router_vision_followup_needs_image") is True + ) + catalog = shared_catalog() + capabilities = catalog.get_capabilities(model, provider_name=provider) + if has_images and capabilities.supports_vision is not True: + raise ModelRouteError(f"Selected tier {selected} has no proven image support") + # Relay adapters can intentionally leave the control dialect unset while + # the catalog still verifies that the underlying model reasons. Admission + # checks that model fact; request controls remain provider-owned. + thinking_budget = common._route_thinking_budget_tokens( + ctx, config, raw, + thinking_mode=thinking_mode or ctx.metadata.get("thinking_mode"), + rollout_phase=config.rollout_phase, + ) + if thinking_budget > 0 and catalog.resolve_entry( + model, provider=provider + ).supports_reasoning is not True: + raise ModelRouteError(f"Selected tier {selected} does not support reasoning") + if ctx.tool_defs and not capabilities.supports_tools: + raise ModelRouteError(f"Selected tier {selected} does not support tool calls") + return ctx + + +def validate_model_route_capacity( + ctx: TurnContext, + *, + tier: str | None = None, + thinking_mode: str | None = None, + allow_compaction_retry: bool = False, +) -> TurnContext: + """Check the selected deployment without any semantic tier substitution.""" + validate_model_route_capabilities(ctx, tier=tier, thinking_mode=thinking_mode) + if ctx.metadata.get("routing_applied") is not True: + return ctx + from opensquilla.engine.steps import squilla_router as common + + config = ctx.config.squilla_router + selected = tier or ctx.metadata.get("routed_tier") + assert isinstance(selected, str) # Validated by the capability gate above. + + def assess(turn: TurnContext): + assessments = common._capacity_tier_assessments( + turn, + config, + config.tiers, + [selected], + minimum_tier=None, + material_tokens=common._final_material_estimated_tokens(turn, turn.semantic_message), + request_input_tokens=common._complete_request_estimated_tokens( + turn, turn.semantic_message + ), + # Deployment capabilities above are authoritative, not the old + # preset supports_image shortcut-routing hint. + requires_image=False, + thinking_mode=thinking_mode or ctx.metadata.get("thinking_mode"), + rollout_phase=config.rollout_phase, + ) + return assessments[0].assessment if assessments else None + + assessment = assess(ctx) + if assessment and assessment.fits: + ctx.metadata.pop("router_model_capacity_retry_pending", None) + return ctx + # Compaction may reduce known historical pressure, never current material, + # and never changes the classified tier. Runtime must re-admit the actual + # compacted request before provider execution. + if ( + allow_compaction_retry + and assessment + and assessment.status == "known_capacity_request_too_large" + and ctx.metadata.get("routing_history_capacity_estimate_complete") is True + and int(ctx.metadata.get("routing_history_capacity_estimated_tokens") or 0) > 0 + ): + current_only = replace( + ctx, + metadata={ + **ctx.metadata, + "routing_history_capacity_estimated_tokens": 0, + "routing_history_capacity_message_count": 0, + }, + ) + reduced = assess(current_only) + if reduced and reduced.fits: + ctx.metadata["router_model_capacity_retry_pending"] = True + return ctx + raise ModelRouteError(f"Selected tier {selected} cannot fit the complete request") diff --git a/src/opensquilla/engine/steps/squilla_router.py b/src/opensquilla/engine/steps/squilla_router.py index 396998a4d1..22d802afd6 100644 --- a/src/opensquilla/engine/steps/squilla_router.py +++ b/src/opensquilla/engine/steps/squilla_router.py @@ -15,7 +15,8 @@ from collections.abc import Mapping from dataclasses import dataclass from inspect import Parameter, signature -from typing import Any, Protocol, cast +from pathlib import Path +from typing import TYPE_CHECKING, Any, Protocol, cast import structlog @@ -75,6 +76,9 @@ ) from opensquilla.token_estimation import estimate_material_text_tokens, estimate_tokens +if TYPE_CHECKING: + from opensquilla.squilla_router.lightgbm_model import LightGBMRouterModel + log = structlog.get_logger(__name__) _log_std = logging.getLogger(__name__) @@ -89,7 +93,7 @@ async def classify( ) -> tuple[str, float, str, dict]: ... -_strategy: RouterStrategy | None = None +_strategy: RouterStrategy | LightGBMRouterModel | None = None _strategy_key: tuple | None = None _strategy_lock = threading.Lock() _router_runtime_warning_lock = threading.Lock() @@ -577,6 +581,8 @@ async def classify( def _strategy_cache_key(config: object) -> tuple: strategy_name = _strategy_name(config) + if strategy_name == "lightgbm_model": + return strategy_name, getattr(config, "lightgbm_model_dir", None) confidence = getattr(config, "confidence_threshold", 0.5) return ( strategy_name, @@ -588,14 +594,9 @@ def _strategy_cache_key(config: object) -> tuple: def _strategy_name(config: object) -> str: - configured = str(getattr(config, "strategy", "v4_phase3") or "v4_phase3") - if configured != "v4_phase3": - log.warning( - "squilla_router.removed_strategy_ignored", - strategy=configured, - using="v4_phase3", - ) - return "v4_phase3" + if getattr(config, "strategy", "lightgbm_model") == "legacy_v4_phase3": + return "v4_phase3" + return "lightgbm_model" def _is_history_strategy(strategy_name: str) -> bool: @@ -628,7 +629,7 @@ def _degraded_fallback_strategy(error: Exception) -> RouterStrategy: return _UnavailableV4Strategy(error) -def _get_strategy(config: object) -> RouterStrategy: +def _get_strategy(config: object) -> RouterStrategy | LightGBMRouterModel: global _strategy, _strategy_key # noqa: PLW0603 with _strategy_lock: key = _strategy_cache_key(config) @@ -637,6 +638,14 @@ def _get_strategy(config: object) -> RouterStrategy: if _strategy_key is not None and _strategy_key != key: _history_store.clear() + if _strategy_name(config) == "lightgbm_model": + from opensquilla.squilla_router.lightgbm_model import LightGBMRouterModel + + # Integrity failures must not substitute an unrelated classifier. + _strategy = LightGBMRouterModel(getattr(config, "lightgbm_model_dir", None)) + _strategy_key = key + return _strategy + base_dir = getattr(config, "v4_bundle_dir", None) def _build(bundle_dir: str | None) -> RouterStrategy: @@ -672,7 +681,14 @@ def _build(bundle_dir: str | None) -> RouterStrategy: return strategy -def preload_strategy(config: object) -> RouterStrategy: +def validate_model_assets(model_dir: Path) -> None: + """Check model assets through the engine boundary without loading inference.""" + from opensquilla.squilla_router.lightgbm_model import validate_artifacts + + validate_artifacts(model_dir) + + +def preload_strategy(config: object) -> RouterStrategy | LightGBMRouterModel: return _get_strategy(config) @@ -743,12 +759,12 @@ def router_runtime_status() -> dict[str, Any]: "error": None, } source = str(getattr(strategy, "source", "") or "") - if source == "v4_phase3" and getattr(strategy, "_available", False): + if source in {"v4_phase3", "lightgbm_model"} and getattr(strategy, "_available", False): return { "initialized": True, "loaded": True, "code": None, - "strategy": "v4_phase3", + "strategy": source, "error": None, } error = getattr(strategy, "error", None) @@ -1285,6 +1301,12 @@ async def finalize_squilla_router_capacity( """ router_cfg = getattr(ctx.config, "squilla_router", None) if ctx.config else None + if ctx.metadata.get("router_tier_policy") == "model": + from opensquilla.engine.steps.model_router import validate_model_route_capacity + + return validate_model_route_capacity( + ctx, allow_compaction_retry=allow_compaction_retry, + ) if ( not router_cfg or not getattr(router_cfg, "enabled", False) @@ -2002,6 +2024,11 @@ async def apply_squilla_router(ctx: TurnContext) -> TurnContext: # decision in the session receives one monotonic identity. routing_turn_index = _ensure_routing_turn_index(ctx) + if _strategy_name(router_cfg) == "lightgbm_model": + from opensquilla.engine.steps.model_router import apply_model_router + + return await apply_model_router(ctx) + # Capacity follows the actual semantic user input, never a shorter # process-local routing hint (for example a Goal objective snapshot). material_estimated_tokens = _material_estimated_tokens(ctx, semantic_message) @@ -2470,7 +2497,8 @@ async def apply_squilla_router(ctx: TurnContext) -> TurnContext: ) return ctx - strategy = _get_strategy(router_cfg) + # The model-policy strategy was dispatched before this legacy classify path. + strategy = cast(RouterStrategy, _get_strategy(router_cfg)) strategy_name = _strategy_name(router_cfg) defer_history = bool(ctx.metadata.get(_DEFER_ROUTING_HISTORY_KEY)) diff --git a/src/opensquilla/engine/stream_recovery.py b/src/opensquilla/engine/stream_recovery.py index d2afd92a9f..047518d89f 100644 --- a/src/opensquilla/engine/stream_recovery.py +++ b/src/opensquilla/engine/stream_recovery.py @@ -38,7 +38,15 @@ def capture( provider=provider, messages=copy.deepcopy(messages), tools=copy.deepcopy(tools), - config=config.model_copy(deep=True, update={"physical_attempt_limit": 1}), + config=config.model_copy(deep=True, update={ + "physical_attempt_limit": 1, + # Credential refresh precedes the original model request. A + # response replay uses those credentials, not another refresh. + "compatibility_recovery": ( + None if config.compatibility_recovery == "refresh_credentials" + else config.compatibility_recovery + ), + }), account_usage=account_usage, provider_id=provider_id, model=model, diff --git a/src/opensquilla/engine/turn_runner/harness.py b/src/opensquilla/engine/turn_runner/harness.py index b391fc96c1..caf3d566fc 100644 --- a/src/opensquilla/engine/turn_runner/harness.py +++ b/src/opensquilla/engine/turn_runner/harness.py @@ -326,6 +326,7 @@ async def run_pipeline( "prev_assistant_text": request.prev_assistant_text, "prev_assistant_usage": request.prev_assistant_usage, "history_user_texts": request.history_user_texts, + "model_router_context": request.model_router_context, "history_capacity_estimated_tokens": ( request.history_capacity_estimated_tokens ), diff --git a/src/opensquilla/engine/turn_runner/prompt_assembler_stage.py b/src/opensquilla/engine/turn_runner/prompt_assembler_stage.py index 89cddc8c14..88b20fbfcc 100644 --- a/src/opensquilla/engine/turn_runner/prompt_assembler_stage.py +++ b/src/opensquilla/engine/turn_runner/prompt_assembler_stage.py @@ -104,6 +104,7 @@ class RunPipelineRequest: prev_assistant_text: str | None = None prev_assistant_usage: dict[str, Any] | None = None history_user_texts: list[str] | None = None + model_router_context: dict[str, Any] | None = field(default=None, repr=False) history_capacity_estimated_tokens: int = 0 history_capacity_message_count: int = 0 history_capacity_estimate_complete: bool = True @@ -577,6 +578,7 @@ async def run( prev_assistant_text=router_context.get("prev_assistant_text"), prev_assistant_usage=router_context.get("prev_assistant_usage"), history_user_texts=router_context.get("history_user_texts"), + model_router_context=router_context, history_capacity_estimated_tokens=max( 0, int(router_context.get("history_capacity_estimated_tokens") or 0), diff --git a/src/opensquilla/gateway/adapters/pending_input_queue.py b/src/opensquilla/gateway/adapters/pending_input_queue.py index 59ae0c6717..fbd0803656 100644 --- a/src/opensquilla/gateway/adapters/pending_input_queue.py +++ b/src/opensquilla/gateway/adapters/pending_input_queue.py @@ -211,7 +211,10 @@ def _enqueue_command(self, raw: dict[str, Any]) -> EnqueuePendingInput: if value is not None: params["displayText"] = value break - for field in ("initialModel", "initial_model", "initialProvider", "initial_provider"): + for field in ( + "initialModel", "initial_model", "initialProvider", "initial_provider", + "routerControlEvent", "router_control_event", + ): if field in raw: params[field] = raw[field] for field in ( diff --git a/src/opensquilla/gateway/adapters/setup_workflow.py b/src/opensquilla/gateway/adapters/setup_workflow.py index 8abb3e64a0..f6866a215c 100644 --- a/src/opensquilla/gateway/adapters/setup_workflow.py +++ b/src/opensquilla/gateway/adapters/setup_workflow.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from typing import Any, cast from opensquilla.application.setup_workflow import SetupCatalog, SetupStatus @@ -170,7 +171,9 @@ async def load_setup_catalog(self) -> SetupCatalog: return setup_catalog() async def load_setup_status(self) -> SetupStatus: - return setup_status(self._config, is_owner=self._is_owner) + # Account bridge refreshes and profile reads are synchronous I/O. Keep + # shutdown/RPC handling responsive while retaining one scoped snapshot. + return await asyncio.to_thread(setup_status, self._config, is_owner=self._is_owner) __all__ = [ diff --git a/src/opensquilla/gateway/adapters/turn_admission.py b/src/opensquilla/gateway/adapters/turn_admission.py index a2a8a27b70..1f286f5750 100644 --- a/src/opensquilla/gateway/adapters/turn_admission.py +++ b/src/opensquilla/gateway/adapters/turn_admission.py @@ -189,6 +189,8 @@ def decode_webchat_command( "initial_model", "initialProvider", "initial_provider", + "routerControlEvent", + "router_control_event", ): if name in params: extra[name] = params[name] diff --git a/src/opensquilla/gateway/admission_input.py b/src/opensquilla/gateway/admission_input.py index b09d9472a3..f673f1e988 100644 --- a/src/opensquilla/gateway/admission_input.py +++ b/src/opensquilla/gateway/admission_input.py @@ -4,7 +4,12 @@ from typing import Any, cast -from opensquilla.application.turn_admission import AdmitTurn, InitialRoutingMode, PendingInputGuard +from opensquilla.application.turn_admission import ( + AdmitTurn, + InitialRoutingMode, + PendingInputGuard, + RouterControlEvent, +) from opensquilla.application.turn_input import ( IncomingTurnSource, MemoryCapturePolicy, @@ -48,6 +53,22 @@ def _initial_model_pin(params: dict[str, Any], camel: str, snake: str) -> str | return values[0] if values else None +def _router_control_event(params: dict[str, Any]) -> RouterControlEvent | None: + values: list[str | None] = [] + for name in ("routerControlEvent", "router_control_event"): + if name not in params: + continue + value = params[name] + if value is not None and ( + not isinstance(value, str) or value not in {"new_task", "redo"} + ): + raise ValueError("routerControlEvent must be new_task or redo") + values.append(value) + if values and any(value != values[0] for value in values[1:]): + raise ValueError("routerControlEvent and router_control_event must match") + return cast(RouterControlEvent | None, values[0] if values else None) + + def normalized_source_hint(params: dict[str, Any]) -> dict[str, Any]: hint = params.get("_source") source = dict(hint) if isinstance(hint, dict) else {} @@ -310,6 +331,7 @@ def decode_admit_turn( # The durable receipt identifies original material, not the shared guarded # text shown for every large paste. Application normalization runs later. fingerprint = dict(fingerprint_params or params) + router_control_event = _router_control_event(params) initial_model = _initial_model_pin(params, "initialModel", "initial_model") initial_provider = _initial_model_pin(params, "initialProvider", "initial_provider") if initial_provider is not None and initial_model is None: @@ -317,6 +339,7 @@ def decode_admit_turn( for camel, snake, value in ( ("initialModel", "initial_model", initial_model), ("initialProvider", "initial_provider", initial_provider), + ("routerControlEvent", "router_control_event", router_control_event), ): fingerprint.pop(camel, None) fingerprint.pop(snake, None) @@ -395,6 +418,7 @@ def decode_admit_turn( initial_routing_mode=cast(InitialRoutingMode | None, routing), initial_model=initial_model, initial_provider=initial_provider, + router_control_event=router_control_event, pending_input=pending_input, required_services=_required_services(params), ) diff --git a/src/opensquilla/gateway/boot.py b/src/opensquilla/gateway/boot.py index 12e2060379..9a41ae6eea 100644 --- a/src/opensquilla/gateway/boot.py +++ b/src/opensquilla/gateway/boot.py @@ -3057,6 +3057,11 @@ async def pause_goals() -> None: def _squilla_router_bundle_dir(router_cfg: Any) -> Path: + if getattr(router_cfg, "strategy", "lightgbm_model") != "legacy_v4_phase3": + configured = getattr(router_cfg, "lightgbm_model_dir", None) + return Path(configured).expanduser() if configured else ( + Path(__file__).resolve().parents[1] / "squilla_router" / "lightgbm_model" / "assets" + ) configured = getattr(router_cfg, "v4_bundle_dir", None) if configured: return Path(configured).expanduser() @@ -3071,9 +3076,11 @@ def validate_squilla_router_runtime(config: GatewayConfig) -> None: if router_cfg is None or not getattr(router_cfg, "enabled", False): return - strategy = getattr(router_cfg, "strategy", "v4_phase3") - if strategy != "v4_phase3": - log.warning("build_services.squilla_router_removed_strategy", strategy=strategy) + if getattr(router_cfg, "strategy", "lightgbm_model") != "legacy_v4_phase3": + from opensquilla.engine.steps.squilla_router import validate_model_assets + + validate_model_assets(_squilla_router_bundle_dir(router_cfg)) + return bundle_dir = _squilla_router_bundle_dir(router_cfg) required = ("runtime_src", "router.runtime.yaml") @@ -3104,10 +3111,10 @@ def validate_squilla_router_runtime_deep(config: GatewayConfig) -> None: error = getattr(strategy, "error", None) if isinstance(error, BaseException): - raise RuntimeError(f"V4 Phase 3 router did not become available: {error}") from error + raise RuntimeError(f"Local model router did not become available: {error}") from error if error is not None: - raise RuntimeError(f"V4 Phase 3 router did not become available: {error}") - raise RuntimeError("V4 Phase 3 router did not become available") + raise RuntimeError(f"Local model router did not become available: {error}") + raise RuntimeError("Local model router did not become available") def _preload_squilla_router_strategy(router_cfg: Any) -> object: @@ -3129,7 +3136,7 @@ async def preload_squilla_router_runtime(config: GatewayConfig) -> None: log.info("gateway.squilla_router_preloaded", bundle_dir=str(bundle_dir)) return if getattr(router_cfg, "require_router_runtime", False): - raise RuntimeError("V4 Phase 3 router did not become available") + raise RuntimeError("Local model router did not become available") log.warning("gateway.squilla_router_preload_unavailable", bundle_dir=str(bundle_dir)) except Exception as exc: # noqa: BLE001 from opensquilla.router_runtime_diagnostics import classify_router_runtime_error diff --git a/src/opensquilla/gateway/config.py b/src/opensquilla/gateway/config.py index 6861cbd8b0..26b264ced0 100644 --- a/src/opensquilla/gateway/config.py +++ b/src/opensquilla/gateway/config.py @@ -1369,7 +1369,9 @@ class SquillaRouterConfig(BaseSettings): enabled: bool = True auto_thinking: bool = True rollout_phase: str = "full" # "observe" | "prompt_only" | "full" - strategy: str = "v4_phase3" + strategy: str = "lightgbm_model" + tier_policy: Literal["model"] = "model" + lightgbm_model_dir: str | None = None tier_profile: str | None = None # Explicit ownership for the router ladder. ``follow_primary`` means # OpenSquilla owns the ladder and may replace it with the active @@ -1451,6 +1453,13 @@ class SquillaRouterConfig(BaseSettings): vision_followup_gate_fallback_recent_turns: int = Field(default=2, ge=0) vision_followup_gate_unknown_policy: str = "image_if_recent" + @field_validator("strategy", mode="before") + @classmethod + def _normalize_router_strategy(cls, value: Any) -> str: + # Existing installations saved v4_phase3 explicitly. Upgrade them to + # the intent-led pair; retain an explicit legacy mode for diagnostics. + return "legacy_v4_phase3" if value == "legacy_v4_phase3" else "lightgbm_model" + @field_validator("visual_mode", mode="before") @classmethod def _normalize_visual_mode(cls, value: Any) -> str: diff --git a/src/opensquilla/gateway/pending_input_primitives.py b/src/opensquilla/gateway/pending_input_primitives.py index 8935894698..214f171c62 100644 --- a/src/opensquilla/gateway/pending_input_primitives.py +++ b/src/opensquilla/gateway/pending_input_primitives.py @@ -78,6 +78,7 @@ def pending_input_payload(turn: AdmitTurn, confirmed_plain_text: bool) -> dict[s ("initialRoutingMode", turn.initial_routing_mode), ("initialModel", turn.initial_model), ("initialProvider", turn.initial_provider), + ("routerControlEvent", turn.router_control_event), ("displayText", turn.display_text), ): if value is not None: @@ -151,7 +152,7 @@ def pending_input_projection( routing = payload.get("initialRoutingMode") if isinstance(routing, str): result["initialRoutingMode"] = routing - for field in ("initialModel", "initialProvider"): + for field in ("initialModel", "initialProvider", "routerControlEvent"): if isinstance(payload.get(field), str): result[field] = payload[field] return cast(PendingInputProjection, result) diff --git a/src/opensquilla/gateway/task_runtime.py b/src/opensquilla/gateway/task_runtime.py index 343f2e8a88..bcd8f411c2 100644 --- a/src/opensquilla/gateway/task_runtime.py +++ b/src/opensquilla/gateway/task_runtime.py @@ -608,6 +608,7 @@ def _reusable_route_envelope(envelope: RouteEnvelope) -> RouteEnvelope: metadata = dict(envelope.metadata) metadata.pop("selected_skills", None) + metadata.pop("router_model_control_event", None) metadata.pop("office_candidate", None) metadata.pop("annotation_selection", None) authority = metadata.get("activation_authority") @@ -4998,7 +4999,9 @@ async def _execute(self, task: _RuntimeTask) -> None: ] if metadata.get("plan_run_id"): turn_context["plan_run_id"] = metadata["plan_run_id"] - for field in ("target_turn_id", "promoted_from_turn_id"): + for field in ( + "target_turn_id", "promoted_from_turn_id", "router_model_control_event", + ): value = metadata.get(field) if isinstance(value, str) and value: turn_context[field] = value @@ -5782,6 +5785,8 @@ def _restart_recovery_envelope( details = target_task.details if isinstance(target_task.details, dict) else {} metadata_raw = details.get("metadata") metadata = dict(metadata_raw) if isinstance(metadata_raw, dict) else {} + # Promoted steering is a new turn, not recovery of an explicit router control. + metadata.pop("router_model_control_event", None) # Promoted steering is a new turn, not recovery of the original Office edit. metadata.pop("office_candidate", None) metadata.pop("annotation_selection", None) @@ -7722,7 +7727,9 @@ async def _record_primary_terminal_disposition( client_request_id = metadata.get("client_request_id") if isinstance(client_request_id, str) and client_request_id: context["client_request_id"] = client_request_id - for context_field in ("target_turn_id", "promoted_from_turn_id"): + for context_field in ( + "target_turn_id", "promoted_from_turn_id", "router_model_control_event", + ): value = metadata.get(context_field) if isinstance(value, str) and value: context[context_field] = value diff --git a/src/opensquilla/gateway/turn_ingress.py b/src/opensquilla/gateway/turn_ingress.py index a32873de30..ade1c81984 100644 --- a/src/opensquilla/gateway/turn_ingress.py +++ b/src/opensquilla/gateway/turn_ingress.py @@ -40,6 +40,7 @@ ), ("initial_model", ("initialModel", "initial_model")), ("initial_provider", ("initialProvider", "initial_provider")), + ("router_control_event", ("routerControlEvent", "router_control_event")), ("fork_before_message_id", ("forkBeforeMessageId", "fork_before_message_id")), ("queue_mode", ("queueMode", "queue_mode")), ("no_memory_capture", ("noMemoryCapture", "no_memory_capture")), diff --git a/src/opensquilla/observability/decision_log.py b/src/opensquilla/observability/decision_log.py index 4a808483b0..1d79ff7382 100644 --- a/src/opensquilla/observability/decision_log.py +++ b/src/opensquilla/observability/decision_log.py @@ -31,6 +31,8 @@ _VISION_GATE_STATIC_REASONS = {"candidate_window_expired"} RoutingSource = Literal[ + "model_tier_policy", + "router_control_hold", "v4_phase3", "v4_unavailable", "cache", diff --git a/src/opensquilla/provider/catalog_overrides.toml b/src/opensquilla/provider/catalog_overrides.toml index 41f93b1a22..916233e630 100644 --- a/src/opensquilla/provider/catalog_overrides.toml +++ b/src/opensquilla/provider/catalog_overrides.toml @@ -169,10 +169,18 @@ cache_write_cost_per_mtok = 2.5 [openrouter."deepseek/deepseek-v4-flash"] context_window = 1024000 max_output_tokens = 384000 +supports_reasoning = true +supports_tools = true +supports_vision = false +reasoning_format = "openrouter" [openrouter."deepseek/deepseek-v4-pro"] context_window = 1048576 max_output_tokens = 393216 +supports_reasoning = true +supports_tools = true +supports_vision = false +reasoning_format = "openrouter" [openrouter."z-ai/glm-5.2"] context_window = 1048576 diff --git a/src/opensquilla/provider/openai.py b/src/opensquilla/provider/openai.py index c056794f84..cb146a5229 100644 --- a/src/opensquilla/provider/openai.py +++ b/src/opensquilla/provider/openai.py @@ -3818,6 +3818,7 @@ async def _stream( # HTTP status solely as diagnostic evidence. event.provider_code = "account_request_uncertain" event.retry_after_s = None + event.compatibility_recovery = None yield event async def _stream_impl( @@ -3840,12 +3841,12 @@ async def _stream_impl( ) stream_timeout_fallback = ( self._compat.stream_timeout_fallback - and not coordinator_owns_retry + and cfg.compatibility_recovery != "non_stream" and non_stream_fallback_allowed ) empty_stream_fallback = ( self._compat.empty_stream_fallback - and not coordinator_owns_retry + and cfg.compatibility_recovery != "non_stream" and non_stream_fallback_allowed ) ( @@ -4166,6 +4167,13 @@ def release_deferred_queue() -> list[StreamEvent]: return released try: + if cfg.compatibility_recovery == "non_stream": + async for event in self._complete_non_stream( + payload=payload, headers=headers, cfg=cfg, tools=tools, + timeout_exc=httpx.ReadTimeout("Coordinator requested non-stream recovery"), + ): + yield event + return client = await self._build_http_client( timeout=( _stream_timeout(cfg.timeout) @@ -5124,6 +5132,12 @@ def release_deferred_queue() -> list[StreamEvent]: and input_tokens == 0 and output_tokens == 0 ): + if coordinator_owns_retry: + yield stream_error( + message="Provider returned an empty stream.", + code="incomplete_stream", compatibility_recovery="non_stream", + ) + return log.warning( "openai.empty_stream_fallback_started", provider=self._provider_kind, @@ -5496,6 +5510,12 @@ def release_deferred_queue() -> list[StreamEvent]: and input_tokens == 0 and output_tokens == 0 ): + if coordinator_owns_retry: + yield stream_error( + message="Provider returned an empty stream.", + code="incomplete_stream", compatibility_recovery="non_stream", + ) + return log.warning( "openai.empty_stream_fallback_started", provider=self._provider_kind, @@ -5600,6 +5620,13 @@ def release_deferred_queue() -> list[StreamEvent]: and not emitted_stream_event and code != CONNECTION_FAILED_CODE ): + if coordinator_owns_retry: + yield stream_error( + message=safe_error, code=code, + recovery_origin=transport_recovery_origin(exc), + compatibility_recovery="non_stream", + ) + return event_name = ( "openrouter.stream_timeout_fallback_started" if self._provider_kind == "openrouter" diff --git a/src/opensquilla/provider/openai_codex.py b/src/opensquilla/provider/openai_codex.py index 26b8a27cc3..b83caf643d 100644 --- a/src/opensquilla/provider/openai_codex.py +++ b/src/opensquilla/provider/openai_codex.py @@ -283,6 +283,10 @@ async def _stream( ) -> AsyncIterator[StreamEvent]: try: credentials = load_codex_credentials(self._auth_path) + if cfg.compatibility_recovery == "refresh_credentials": + credentials = await refresh_codex_credentials( + credentials, path=self._auth_path, proxy=self._proxy, + ) except CodexAuthError as exc: yield ErrorEvent(message=str(exc), code="401") return @@ -368,7 +372,7 @@ async def _stream( trust_env=_trust_env(), proxy=self._proxy, ) as client: - refreshed = False + refreshed = cfg.compatibility_recovery == "refresh_credentials" while True: async with client.stream( "POST", @@ -392,6 +396,17 @@ async def _stream( yield ErrorEvent(message=str(exc), code="401") return continue + if ( + response.status_code == 401 and coordinator_owns_retry + and cfg.compatibility_recovery != "refresh_credentials" + and credentials.refresh_token + ): + yield ErrorEvent( + message="Codex authorization requires credential refresh.", + code="401", http_status=401, + compatibility_recovery="refresh_credentials", + ) + return if response.status_code != 200: body = await response.aread() yield ErrorEvent( diff --git a/src/opensquilla/provider/preset_registry.py b/src/opensquilla/provider/preset_registry.py index 2c34f1cd1e..a092ce8f36 100644 --- a/src/opensquilla/provider/preset_registry.py +++ b/src/opensquilla/provider/preset_registry.py @@ -66,6 +66,10 @@ # edited or mixed-provider ladder remains operator-owned. PREVIOUS_RECOMMENDED_TEXT_MODELS: dict[str, frozenset[tuple[str, ...]]] = { "openrouter": frozenset({ + ( + "qwen/qwen3.7-flash", "deepseek/deepseek-v4-flash-0731", + "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3", + ), ( "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "z-ai/glm-5.2", "z-ai/glm-5.2", @@ -76,6 +80,7 @@ ), }), "tokenrhythm": frozenset({ + ("qwen3.7-flash", "deepseek-flash", "deepseek-v4-pro-0813", "glm-5.3"), ("deepseek-v4-flash", "deepseek-v4-pro", "kimi-k2.7-code", "glm-5.1"), ("deepseek-v4-flash", "deepseek-v4-pro", "kimi-k2.7-code", "glm-5.2"), ("qwen3.7-flash", "deepseek-v4-flash-0731", "glm-5.2", "glm-5.2"), diff --git a/src/opensquilla/provider/presets/openrouter.toml b/src/opensquilla/provider/presets/openrouter.toml index b649053a49..e72bd381bc 100644 --- a/src/opensquilla/provider/presets/openrouter.toml +++ b/src/opensquilla/provider/presets/openrouter.toml @@ -17,22 +17,22 @@ supports_image = false [tiers.c1] provider = "openrouter" -model = "deepseek/deepseek-v4-flash-0731" -description = "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis" +model = "deepseek/deepseek-v4-flash" +description = "Default DeepSeek V4 Flash route for normal agent work, coding assistance, debugging, and moderate analysis" thinking_level = "high" supports_image = false [tiers.c2] provider = "openrouter" -model = "deepseek/deepseek-v4-pro-0813" -description = "Stronger DeepSeek V4 Pro 0813 route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis" +model = "deepseek/deepseek-v4-pro" +description = "Stronger DeepSeek V4 Pro route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis" thinking_level = "high" supports_image = false [tiers.c3] provider = "openrouter" -model = "z-ai/glm-5.3" -description = "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" +model = "moonshotai/kimi-k3" +description = "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" thinking_level = "high" supports_image = false diff --git a/src/opensquilla/provider/presets/tokenrhythm.toml b/src/opensquilla/provider/presets/tokenrhythm.toml index e04c311692..d0c76cb68e 100644 --- a/src/opensquilla/provider/presets/tokenrhythm.toml +++ b/src/opensquilla/provider/presets/tokenrhythm.toml @@ -11,7 +11,7 @@ id = "tokenrhythm" provider = "tokenrhythm" label = "TokenRhythm" -description = "Curated TokenRhythm router ladder (Qwen3.7 Flash, DeepSeek Flash, DeepSeek V4 Pro 0813, then GLM 5.3), applied as inline tiers." +description = "Curated TokenRhythm router ladder (Qwen3.7 Flash, DeepSeek V4 Flash 0731, DeepSeek V4 Pro 0813, then Kimi K3), applied as inline tiers." default_model = "deepseek-v4-pro-0813" [tiers.c0] @@ -22,8 +22,8 @@ supports_image = false [tiers.c1] provider = "tokenrhythm" -model = "deepseek-flash" -description = "Default DeepSeek Flash route for normal agent work, coding assistance, debugging, and moderate analysis" +model = "deepseek-v4-flash-0731" +description = "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis" supports_image = false [tiers.c2] @@ -34,8 +34,8 @@ supports_image = false [tiers.c3] provider = "tokenrhythm" -model = "glm-5.3" -description = "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" +model = "kimi-k3" +description = "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis" supports_image = false ensemble_enabled = false diff --git a/src/opensquilla/provider/types.py b/src/opensquilla/provider/types.py index 269a919fd0..a74bf1b612 100644 --- a/src/opensquilla/provider/types.py +++ b/src/opensquilla/provider/types.py @@ -223,6 +223,9 @@ class ToolArgumentRejection: terminal_reason: str +ProviderCompatibilityRecovery = Literal["non_stream", "refresh_credentials"] + + @dataclass class ErrorEvent: """Stream error. @@ -262,6 +265,8 @@ class ErrorEvent: # Trusted transport evidence, distinct from broad retry classifications. # Unknown extensions and protocol/consumer failures cannot authorize replay. recovery_origin: RecoveryOrigin = "unknown" + # Adapter evidence for a coordinator-owned next attempt; never a hidden resend. + compatibility_recovery: ProviderCompatibilityRecovery | None = None @dataclass(frozen=True, slots=True) @@ -720,6 +725,10 @@ class ChatConfig(BaseModel): exclude=True, repr=False, ) + # A budgeted coordinator request, excluded from all provider wire payloads. + compatibility_recovery: ProviderCompatibilityRecovery | None = Field( + default=None, exclude=True, repr=False, + ) # Runtime-only absolute turn deadline. Provider selectors use it to avoid # violating an upstream Retry-After when the same account/endpoint owns the # next fallback leg. Adapters never serialize or send this value upstream. diff --git a/src/opensquilla/router_runtime_diagnostics.py b/src/opensquilla/router_runtime_diagnostics.py index 880da9bc15..d3efc639fd 100644 --- a/src/opensquilla/router_runtime_diagnostics.py +++ b/src/opensquilla/router_runtime_diagnostics.py @@ -44,7 +44,7 @@ def classify_router_runtime_error(error: BaseException | str | None) -> str: text = _error_text(error) lower = text.lower() - if "missing v4 bundle files" in lower: + if "missing v4 bundle files" in lower or "missing lightgbm" in lower: return ROUTER_ASSETS_MISSING if ( "libomp.dylib" in lower @@ -93,7 +93,7 @@ def router_runtime_hint(kind: str) -> str: return ( "Bundled SquillaRouter assets are missing. Reinstall " "`opensquilla[recommended]`, or if running from source, run " - '`git lfs pull --include="src/opensquilla/squilla_router/models/**"`.' + '`git lfs pull --include="src/opensquilla/squilla_router/**"`.' ) if kind == ROUTER_PYTHON_DEPENDENCY_MISSING: return ( diff --git a/src/opensquilla/session/manager.py b/src/opensquilla/session/manager.py index 53efcb7fcc..fad48b28c5 100644 --- a/src/opensquilla/session/manager.py +++ b/src/opensquilla/session/manager.py @@ -2977,6 +2977,116 @@ async def remove_message(self, session_key: str, message_id: str) -> bool: return False return await self._storage.delete_transcript_entry(node.session_id, message_id) + async def get_model_router_transcript( + self, + session_key: str, + *, + expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> list[TranscriptEntry]: + """Read model-policy state across compaction under one session owner.""" + session_key = canonicalize_session_key(session_key) + node = await self._storage.get_session(session_key) + if node is None: + raise KeyError(f"Session not found: {session_key}") + _require_compatible_session_owner( + node, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, operation="model router read", + ) + rows = await self._storage.get_canonical_transcript(node.session_id) + from opensquilla.session.model_router_state import MODEL_DECISION_KEY + + state_rows = [ + entry for entry in rows + if entry.role == "user" + and isinstance(record := (entry.turn_context or {}).get(MODEL_DECISION_KEY), dict) + and record.get("status") == "admitted" and record.get("state_committed") is True + ][-2:] + # A provider failure can finish without an assistant row. Reuse the + # ownership-checked task/fork outcome projection rather than guessing + # success from an older assistant answer. + outcomes = await self._resolve_fork_terminal_outcomes( + node, state_rows, target_session_id=node.session_id, + target_session_key=node.session_key, + ) + for entry in state_rows: + projection = outcomes.projections.get(_entry_turn_id(entry) or "") + if projection is not None: + entry.turn_context = { + **(entry.turn_context or {}), + "router_model_previous_outcome": dict(projection.get("outcome") or {}), + } + current = await self._storage.get_session(session_key) + if current is None or current.session_id != node.session_id or current.epoch != node.epoch: + raise StaleEpochError("Session owner changed during model router read") + return rows + + async def stage_model_router_decision( + self, + session_key: str, + message_id: str, + decision: dict[str, Any], + *, + expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> dict[str, Any]: + """Persist a classification before deployment admission.""" + session_key = canonicalize_session_key(session_key) + node = await self._storage.get_session(session_key) + if node is None: + raise KeyError(f"Session not found: {session_key}") + _require_compatible_session_owner( + node, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, operation="model router stage", + ) + return await self._storage.stage_model_router_decision( + session_key, message_id, decision, + expected_session_id=node.session_id, expected_epoch=int(node.epoch or 0), + ) + + async def admit_model_router_decision( + self, + session_key: str, + message_id: str, + *, + expected_previous_version: int | None, + expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> dict[str, Any] | None: + """Advance task state after selected-deployment preflight under the same owner.""" + session_key = canonicalize_session_key(session_key) + node = await self._storage.get_session(session_key) + if node is None: + raise KeyError(f"Session not found: {session_key}") + _require_compatible_session_owner( + node, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, operation="model router admission", + ) + return await self._storage.admit_model_router_decision( + session_key, message_id, expected_session_id=node.session_id, + expected_epoch=int(node.epoch or 0), + expected_previous_version=expected_previous_version, + ) + + async def advance_model_router_quality_retry( + self, session_key: str, message_id: str, *, expected_session_id: str | None, + expected_session_epoch: int | None, expected_version: int, target_tier: str, + reason: str, scope_id: str, execution_update: dict[str, Any], + ) -> dict[str, Any] | None: + node = await self._storage.get_session(session_key) + if node is None: + raise KeyError(f"Session not found: {session_key}") + _require_compatible_session_owner( + node, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, operation="model quality retry", + ) + return await self._storage.advance_model_router_quality_retry( + session_key, message_id, expected_session_id=node.session_id, + expected_epoch=int(node.epoch or 0), expected_version=expected_version, + target_tier=target_tier, reason=reason, scope_id=scope_id, + execution_update=execution_update, + ) + async def update_message_turn_context( self, session_key: str, diff --git a/src/opensquilla/session/model_router_state.py b/src/opensquilla/session/model_router_state.py new file mode 100644 index 0000000000..b84e9f0227 --- /dev/null +++ b/src/opensquilla/session/model_router_state.py @@ -0,0 +1,558 @@ +"""Durable model-policy inputs anchored to accepted transcript rows. + +Policy state lives on the input that produced it, before provider execution. +Canonical transcript copies therefore preserve fork prefixes, while session +identity rotation and compaction retain their existing ownership semantics. +""" + +from __future__ import annotations + +import json +import math +import re +import time +from collections.abc import Mapping, Sequence +from copy import deepcopy +from typing import Any + +MODEL_DECISION_KEY = "router_model_decision" + + +def canonical_previous_usage(value: object) -> dict[str, Any]: + """Match the frozen feature snapshot's complete optional usage bundle.""" + if not isinstance(value, Mapping): + return {} + required = { + "input_tokens", + "output_tokens", + "reasoning_tokens", + "cached_tokens", + "cache_write_tokens", + } + if not required.issubset(value) or any( + isinstance(value[key], bool) or not isinstance(value[key], int) or value[key] < 0 + for key in required + ): + return {} + execution = { + "route_id", + "execution_status", + "error_code", + "response_id", + "attempt_ids", + "retry_count", + } + return {key: item for key, item in value.items() if key not in execution} + + +def visible_router_text(role: str, content: object) -> str: + from opensquilla.engine.steps.inject_time_prefix import TIME_PREFIX_RE + + text = str(content or "") + if text.lstrip().startswith("{"): + try: + payload = json.loads(text) + except (TypeError, ValueError): + payload = None + if isinstance(payload, Mapping): + visible = payload.get("text") + envelope = (role == "user" and "attachments" in payload) or ( + role == "assistant" and "artifacts" in payload + ) + if isinstance(visible, str) and envelope: + text = visible + if role == "user": + text = TIME_PREFIX_RE.sub("", text, count=1) + return text.strip() + + +def normalized_router_attachments(attachments: Sequence[object]) -> list[dict[str, Any]]: + """Source all-or-nothing modality projection; never include attachment bytes.""" + normalized = [] + for attachment in attachments: + if not isinstance(attachment, Mapping): + return [] + raw = next( + ( + value.strip().casefold() + for key in ("mime", "mime_type", "content_type", "type") + if isinstance((value := attachment.get(key)), str) and value.strip() + ), + None, + ) + if raw is None: + return [] + media = raw.split(";", 1)[0].strip() + if media.startswith("image/") or media == "image": + modality = "image" + elif media.startswith("audio/") or media == "audio": + modality = "audio" + elif media.startswith("video/") or media == "video": + modality = "video" + elif ( + media.startswith("text/") + or media + in { + "application/json", + "application/pdf", + "application/rtf", + "application/xml", + "document", + } + or media.startswith("application/vnd.openxmlformats-officedocument") + or media.startswith("application/vnd.oasis.opendocument") + ): + modality = "document" + elif media in { + "application/gzip", + "application/x-7z-compressed", + "application/x-rar-compressed", + "application/x-tar", + "application/zip", + "archive", + } or media.endswith("+zip"): + modality = "archive" + else: + modality = "other" + safe: dict[str, Any] = {"type": modality} + for key in ( + "mime_type", + "media_type", + "parse_status", + "status", + "summary", + "truncated", + "token_count", + ): + value = attachment.get(key) + if isinstance(value, (str, int, float, bool)) or value is None: + safe[key] = value + normalized.append(safe) + return normalized + + +def prepare_model_router_metadata( + context: Mapping[str, Any] | None, + *, + current_request: str, + input_message_id: str | None, + history_user: Sequence[str] | None = None, + previous_answer: str | None = None, + previous_usage: Mapping[str, Any] | None = None, + tool_defs: Sequence[Any] = (), + attachments: Sequence[object] = (), + channel_kind: str = "", +) -> dict[str, Any]: + """Complete the frozen RouterInput contract with this turn's raw request facts.""" + context = context or {} + model_input = deepcopy(context.get("router_model_input") or {}) + model_input["current_request"] = current_request + model_input.setdefault("task_anchor", "") + model_input.setdefault("history_user", list(history_user or [])[-4:]) + model_input.setdefault("previous_answer", previous_answer or "") + model_input.setdefault("previous_usage", dict(previous_usage or {})) + model_input.setdefault("previous_outcome", "unknown") + model_input.setdefault("active_route_tier", None) + model_input.setdefault("route_history", []) + history_chars = context.get("router_model_history_user_chars") + if history_chars is None: + history_chars = sum(len(text) for text in model_input["history_user"]) + model_input["context"] = { + "turn_index": len(model_input["route_history"]), + "context_tokens_est": ( + len(current_request) + history_chars + len(model_input["previous_answer"]) + ) // 4, + } + available_tools = sorted({name for tool in tool_defs if ( + name := str(tool.get("name") if isinstance(tool, Mapping) + else getattr(tool, "name", "")).strip() + )}) + model_input["tool_state"] = {"available_tools": available_tools} if available_tools else {} + model_input["attachments"] = normalized_router_attachments(attachments) + model_input["previous_usage"] = canonical_previous_usage(model_input["previous_usage"]) + if channel_kind: + model_input["context"]["platform"] = channel_kind + control_context = dict(context.get("router_model_control_context") or {}) + control_context["context_tokens_est"] = ( + int(control_context.get("context_tokens_est") or 0) + + max(1, (len(current_request) + 3) // 4) + ) + metadata = { + "router_model_control_context": control_context, + "router_model_input": model_input, + "router_model_state": deepcopy(context.get("router_model_state")), + "router_model_input_message_id": input_message_id, + } + if context.get("router_model_decision"): + metadata["router_model_decision"] = deepcopy(context["router_model_decision"]) + if context.get("router_model_control_event") is not None: + metadata["router_model_control_event"] = context["router_model_control_event"] + return metadata + + +def model_generation_transcript( + entries: Sequence[Any], + bound_user_message_id: str | None, + *, + include_current_staged: bool = False, +) -> list[Any] | None: + """Return the explicit-reset generation, or None for ordinary history replay.""" + rows = list(entries) + boundary = ( + next( + ( + i + for i, row in enumerate(rows) + if getattr(row, "message_id", None) == bound_user_message_id + ), + None, + ) + if bound_user_message_id + else None + ) + state_rows = [ + row + for i, row in enumerate(rows) + if getattr(row, "role", None) == "user" + and (boundary is None or i <= boundary) + and _decision(row).get("state") + and ( + _decision(row).get("state_committed") is True + or ( + include_current_staged and getattr(row, "message_id", None) == bound_user_message_id + ) + ) + ] + state = _mapping(_decision(state_rows[-1]).get("state")) if state_rows else {} + generation_start = state.get("generation_start_input_message_id") + if not generation_start: + return None + start = next( + ( + i + for i, row in enumerate(rows) + if generation_start + in {getattr(row, "message_id", None), _decision(row).get("input_message_id")} + ), + None, + ) + if start is None: + raise RuntimeError("Model router generation boundary is absent from canonical history") + allowed_turn_ids = { + _turn_id(row) + for i, row in enumerate(rows) + if i >= start + and (boundary is None or i <= boundary) + and getattr(row, "role", None) == "user" + } - {""} + return [ + row + for i, row in enumerate(rows) + if i >= start + and ( + (getattr(row, "role", None) == "user" and (boundary is None or i <= boundary)) + or ( + getattr(row, "role", None) != "user" + and ( + _turn_id(row) in allowed_turn_ids + or (not _turn_id(row) and (boundary is None or i <= boundary)) + ) + ) + ) + ] + + +def _mapping(value: object) -> Mapping[str, Any]: + return value if isinstance(value, Mapping) else {} + + +def _decision(entry: Any) -> Mapping[str, Any]: + return _mapping(_mapping(getattr(entry, "turn_context", None)).get(MODEL_DECISION_KEY)) + + +def _turn_id(entry: Any) -> str: + from opensquilla.turn_outcome_projection import turn_id_from_context + + return turn_id_from_context(getattr(entry, "turn_context", None)) or "" + + +def build_model_router_context( + entries: Sequence[Any], + *, + bound_user_message_id: str | None, + exclude_last_user: bool, + feature_entries: Sequence[Any] | None = None, + canonical_complete: bool = True, +) -> dict[str, Any]: + """Build raw user history and the last five admitted routes within 30 minutes.""" + rows = list(entries) + bound_index = ( + next( + ( + i + for i, row in enumerate(rows) + if getattr(row, "message_id", None) == bound_user_message_id + ), + None, + ) + if bound_user_message_id + else None + ) + if bound_user_message_id and bound_index is None: + raise RuntimeError("Model router input is absent from its canonical transcript") + if bound_index is None and exclude_last_user: + bound_index = next( + (i for i in reversed(range(len(rows))) if getattr(rows[i], "role", None) == "user"), + None, + ) + prior_users = [ + row + for i, row in enumerate(rows) + if getattr(row, "role", None) == "user" and (bound_index is None or i < bound_index) + ] + current = rows[bound_index] if bound_index is not None else None + current_id = getattr(current, "message_id", None) + state_rows = [ + row + for row in prior_users + if _decision(row).get("state_committed") is True and _decision(row).get("state") + ] + previous_record = _decision(state_rows[-1]) if state_rows else {} + state = _mapping(previous_record.get("state")) or None + replay_record = _decision(current) if current is not None else {} + active_feature_rows = list(feature_entries) if feature_entries is not None else rows + feature_rows = active_feature_rows + anchor_id = (state or {}).get("task_start_input_message_id") + + def has_id(candidates: Sequence[Any], message_id: str | None) -> bool: + return message_id is not None and any( + message_id + in { + getattr(row, "message_id", None), + _decision(row).get("input_message_id"), + } + for row in candidates + ) + + if anchor_id and not has_id(feature_rows, anchor_id): + if canonical_complete and has_id(rows, anchor_id) and has_id(rows, current_id): + feature_rows = rows + else: + raise RuntimeError("Model router task feature boundary is unavailable") + feature_bound_index = next( + ( + i + for i, row in enumerate(feature_rows) + if row is current or getattr(row, "message_id", None) == current_id + ), + None, + ) + if current_id is not None and feature_bound_index is None: + raise RuntimeError("Model router current feature boundary is unavailable") + feature_prior_users = [ + row + for i, row in enumerate(feature_rows) + if getattr(row, "role", None) == "user" + and (feature_bound_index is None or i < feature_bound_index) + ] + excluded_turns = { + _turn_id(row) + for i, row in enumerate(feature_rows) + if feature_bound_index is not None + and i >= feature_bound_index + and getattr(row, "role", None) == "user" + } - {""} + + def text(row: Any) -> str: + return visible_router_text(str(getattr(row, "role", "")), getattr(row, "content", None)) + + anchor = "" + if state: + task_anchor = state.get("task_start_input_message_id") + for row in feature_prior_users: + record = _decision(row) + aliases = {getattr(row, "message_id", None), record.get("input_message_id")} + if task_anchor in aliases and task_anchor is not None: + anchor = text(row) + break + if not anchor: + # Forks reassign message IDs; task identity also survives the copy. + anchor_row = next( + ( + row + for row in state_rows + if _mapping(_decision(row).get("state")).get("task_id") == state.get("task_id") + ), + None, + ) + if anchor_row is not None: + anchor = text(anchor_row) + + if state_rows: + previous_input = state_rows[-1] + previous_turn_id = _turn_id(previous_input) + previous_index = next( + ( + i + for i, row in enumerate(feature_rows) + if getattr(row, "message_id", None) == getattr(previous_input, "message_id", None) + ), + -1, + ) + # The preceding reply may land after this input was queued. Causal IDs, + # rather than its physical row position, distinguish it from future turns. + answer = None + for i in reversed(range(len(feature_rows))): + row = feature_rows[i] + turn_id = _turn_id(row) + if ( + getattr(row, "role", None) == "assistant" + and turn_id not in excluded_turns + and (turn_id or feature_bound_index is None or i < feature_bound_index) + and (not state_rows or i > previous_index and turn_id in {"", previous_turn_id}) + ): + answer = row + break + usage = _mapping(getattr(answer, "turn_usage", None)) if answer is not None else {} + outcome = "unknown" + execution_succeeded = False + if state_rows: + previous_outcome = _mapping( + _mapping(getattr(state_rows[-1], "turn_context", None)).get( + "router_model_previous_outcome" + ) + ) + outcome_kind = previous_outcome.get("kind") + if outcome_kind == "completed": + execution_succeeded = True + elif outcome_kind in {"failed", "interrupted", "blocked", "budgetLimited", "partial"}: + outcome = "failure" + if outcome != "failure": + signals = [] + for container in ( + getattr(answer, "turn_context", None), + getattr(answer, "turn_usage", None), + ): + for key in ( + "agent_loop_stop_reason", + "stop_reason", + "outcome", + "execution_status", + "error_code", + ): + value = _mapping(container).get(key) + if isinstance(value, str) and value.strip(): + signals.append(value.strip()) + tool_names = [] + for raw in getattr(answer, "tool_calls", None) or []: + if isinstance(raw, Mapping): + name = ( + raw.get("name") + or raw.get("tool_name") + or _mapping(raw.get("function")).get("name") + ) + if isinstance(name, str): + tool_names.append(name) + if re.search( + r"(?:\b(?:clarif(?:y|ication)|ask[_ -]?user|need[_ -]?input)\b|" + r"could you (?:clarify|provide)|please (?:specify|provide)|clarify which|" + r"能否|请\s*提供|需要(?:更多|具体).{0,8}信息)", + " ".join([*signals, *tool_names, text(answer) if answer is not None else ""]), + re.IGNORECASE, + ): + outcome = "clarification" + elif execution_succeeded: + outcome = "success" + elif any( + re.search( + r"(?:\b(?:error|fail(?:ed|ure)?|timeout|abort(?:ed)?|cancel(?:led|ed)?)\b|" + r"错误|失败|超时|取消)", + signal, + re.IGNORECASE, + ) + for signal in signals + ): + outcome = "failure" + elif signals: + outcome = "success" + route_history = [] + history_before = int(time.time() * 1000) + history_cutoff = history_before - 30 * 60 * 1000 + recent_rows = [ + row + for row in state_rows + if int(_decision(row).get("decided_at_ms") or getattr(row, "created_at", 0) or 0) + >= history_cutoff + and int(_decision(row).get("decided_at_ms") or getattr(row, "created_at", 0) or 0) + <= history_before + ] + for row in recent_rows[-5:]: + record = _decision(row) + row_state = _mapping(record.get("state")) + tier = str(row_state.get("tier") or "").upper() + if tier in {"C0", "C1", "C2", "C3"}: + history_entry: dict[str, Any] = {"tier_id": tier} + audit = _mapping(_mapping(record.get("decision")).get("tier_audit")) + probabilities = audit.get("probabilities") + if isinstance(probabilities, Mapping): + try: + vector = { + str(key).upper(): float(value) for key, value in probabilities.items() + } + except (TypeError, ValueError): + vector = {} + if set(vector) == {"C0", "C1", "C2", "C3"} and all( + math.isfinite(p) for p in vector.values() + ): + ordered = sorted(vector.values(), reverse=True) + history_entry.update( + difficulty=sum(i * vector[f"C{i}"] for i in range(4)), + margin=ordered[0] - ordered[1], + ) + route_history.append(history_entry) + user_history = [visible for row in feature_prior_users if (visible := text(row))] + result: dict[str, Any] = { + "router_model_state": deepcopy(state), + "router_model_history_user_chars": sum(len(value) for value in user_history), + "router_model_input": { + "task_anchor": anchor, + "history_user": user_history[-4:], + "previous_answer": text(answer) if answer is not None else "", + "previous_usage": canonical_previous_usage(usage), + "previous_outcome": outcome, + "active_route_tier": str(state["tier"]).upper() if state else None, + "route_history": route_history, + }, + } + from opensquilla.session.compaction import estimate_entry_model_replay_tokens + + generation_rows = model_generation_transcript(rows, bound_user_message_id) + if generation_rows is None: + generation_rows = active_feature_rows + generation_current = next( + ( + i + for i, row in enumerate(generation_rows) + if row is current or getattr(row, "message_id", None) == current_id + ), + None, + ) + generation_history = ( + generation_rows[:generation_current] if generation_current is not None else generation_rows + ) + current_context = ( + _mapping(getattr(current, "turn_context", None)) if current is not None else {} + ) + control = current_context.get("router_model_control_event") + if control in {"new", "new_task", "new-task", "create_task", "create-task"}: + generation_history = [] + result["router_model_control_context"] = { + "context_tokens_est": sum( + estimate_entry_model_replay_tokens(row) for row in generation_history + ), + } + if replay_record: + result["router_model_decision"] = deepcopy(replay_record) + if control in {"new", "new_task", "new-task", "create_task", "create-task", "continue", "redo"}: + result["router_model_control_event"] = control + return result diff --git a/src/opensquilla/session/storage.py b/src/opensquilla/session/storage.py index b1c4694330..4e1d9844c0 100644 --- a/src/opensquilla/session/storage.py +++ b/src/opensquilla/session/storage.py @@ -17715,6 +17715,188 @@ async def delete_transcript_entry(self, session_id: str, message_id: str) -> boo removed = cur.rowcount or 0 return removed > 0 + async def _read_model_router_record( + self, conn: Any, session_id: str, message_id: str, + ) -> tuple[dict[str, Any] | None, bool]: + """Read one accepted input across the active and compacted transcripts.""" + from opensquilla.session.model_router_state import MODEL_DECISION_KEY + + record = None + matched = False + for table in ("transcript_entries", "compacted_transcript_entries"): + async with conn.execute( + f"SELECT turn_context FROM {table} " + "WHERE session_id=? AND message_id=? AND role='user'", + (session_id, message_id), + ) as cur: + row = await cur.fetchone() + if row is None: + continue + matched = True + candidate = json.loads(row[0]).get(MODEL_DECISION_KEY) if row[0] else None + if isinstance(candidate, dict): + if record is not None and record != candidate: + raise RuntimeError("Canonical model router decisions disagree") + record = candidate + return record, matched + + async def _latest_model_router_version(self, conn: Any, session_id: str) -> int | None: + latest_version = None + for table in ("transcript_entries", "compacted_transcript_entries"): + async with conn.execute( + "SELECT MAX(json_extract(turn_context, " + "'$.router_model_decision.state.version')) " + f"FROM {table} WHERE session_id=? AND role='user' " + "AND json_extract(turn_context, " + "'$.router_model_decision.state_committed')=1 " + "AND json_extract(turn_context, " + "'$.router_model_decision.status')='admitted'", + (session_id,), + ) as cur: + latest = await cur.fetchone() + if latest is not None and latest[0] is not None: + latest_version = max(latest_version or 0, int(latest[0])) + return latest_version + + async def _write_model_router_record( + self, conn: Any, session_id: str, message_id: str, record: dict[str, Any], + ) -> None: + from opensquilla.session.model_router_state import MODEL_DECISION_KEY + + encoded = json.dumps(record, allow_nan=False) + for table in ("transcript_entries", "compacted_transcript_entries"): + await conn.execute( + f"UPDATE {table} SET turn_context=" + "json_set(COALESCE(turn_context, '{}'), ?, json(?)) " + "WHERE session_id=? AND message_id=? AND role='user'", + (f"$.{MODEL_DECISION_KEY}", encoded, session_id, message_id), + ) + + async def stage_model_router_decision( + self, + session_key: str, + message_id: str, + decision: dict[str, Any], + *, + expected_session_id: str, + expected_epoch: int, + ) -> dict[str, Any]: + """Persist classification without advancing the admitted task state.""" + staged: dict[str, Any] = json.loads(json.dumps(decision, allow_nan=False)) + staged.update(status="staged", state_committed=False) + staged.setdefault("decided_at_ms", _now_ms()) + async with self._write_transaction("stage_model_router_decision") as conn: + if not await _matches_session_owner_on_conn( + conn, session_key=session_key, session_id=expected_session_id, + session_epoch=expected_epoch, + ): + await self._raise_stale_epoch( + conn, session_key=session_key, expected_epoch=expected_epoch, + expected_session_id=expected_session_id, + ) + committed, matched = await self._read_model_router_record( + conn, expected_session_id, message_id, + ) + if not matched: + raise RuntimeError("Model router decision has no accepted user transcript row") + if committed is not None: + return committed + await self._write_model_router_record(conn, expected_session_id, message_id, staged) + return staged + + async def admit_model_router_decision( + self, + session_key: str, + message_id: str, + *, + expected_session_id: str, + expected_epoch: int, + expected_previous_version: int | None, + ) -> dict[str, Any] | None: + """Admit a staged route only if its prior admitted task state is unchanged.""" + if expected_previous_version is not None and ( + type(expected_previous_version) is not int or expected_previous_version < 1 + ): + raise ValueError("Previous model router version must be positive or absent") + async with self._write_transaction("admit_model_router_decision") as conn: + if not await _matches_session_owner_on_conn( + conn, session_key=session_key, session_id=expected_session_id, + session_epoch=expected_epoch, + ): + await self._raise_stale_epoch( + conn, session_key=session_key, expected_epoch=expected_epoch, + expected_session_id=expected_session_id, + ) + record, _ = await self._read_model_router_record(conn, expected_session_id, message_id) + latest_version = await self._latest_model_router_version(conn, expected_session_id) + if record is None: + raise RuntimeError("Model router admission has no staged decision") + if record.get("status") == "admitted" and record.get("state_committed") is True: + return record + if record.get("status") != "staged" or record.get("state_committed") is not False: + raise RuntimeError("Model router decision has invalid admission state") + state = record.get("state") or {} + proposed_version = state.get("version") + if ( + type(proposed_version) is not int + or proposed_version != (expected_previous_version or 0) + 1 + ): + raise RuntimeError("Model router proposed version does not follow prior state") + if latest_version != expected_previous_version: + return None + record.update( + status="admitted", state_committed=True, + state_version_before=expected_previous_version, admitted_at_ms=_now_ms(), + ) + await self._write_model_router_record(conn, expected_session_id, message_id, record) + return record + + async def advance_model_router_quality_retry( + self, session_key: str, message_id: str, *, expected_session_id: str, + expected_epoch: int, expected_version: int, target_tier: str, + reason: str, scope_id: str, execution_update: dict[str, Any], + ) -> dict[str, Any] | None: + """CAS one effective retry tier without rewriting the original model audit.""" + if ( + reason not in {"validation_failure", "no_progress"} + or execution_update.get("tier") != target_tier + ): + return None + async with self._write_transaction("advance_model_router_quality_retry") as conn: + if not await _matches_session_owner_on_conn( + conn, session_key=session_key, session_id=expected_session_id, + session_epoch=expected_epoch, + ): + await self._raise_stale_epoch( + conn, session_key=session_key, expected_epoch=expected_epoch, + expected_session_id=expected_session_id, + ) + record, _ = await self._read_model_router_record(conn, expected_session_id, message_id) + latest_version = await self._latest_model_router_version(conn, expected_session_id) + if latest_version is not None and latest_version > expected_version: + return None + if record is None: + raise RuntimeError("Quality retry has no durable model routing decision") + if record.get("status") != "admitted" or record.get("state_committed") is not True: + return None + state = record.get("state") or {} + old_tier = state.get("tier") + tiers = ("c0", "c1", "c2", "c3") + if ( + state.get("version") != expected_version + or record.get("quality_retry", {}).get("used") + or old_tier not in tiers or old_tier == "c3" + or target_tier != tiers[tiers.index(old_tier) + 1] + ): + return None + record["state"] = {**state, "tier": target_tier, "version": expected_version + 1} + record["quality_retry"] = {"used": True, "reason": reason, "scope_id": scope_id, + "from_tier": old_tier, "to_tier": target_tier} + record["execution_update"] = dict(execution_update) + record["updated_at_ms"] = _now_ms() + await self._write_model_router_record(conn, expected_session_id, message_id, record) + return record + async def update_transcript_turn_context( self, session_key: str, @@ -17732,9 +17914,20 @@ async def update_transcript_turn_context( async with self._write_transaction("update_transcript_turn_context") as conn: for table in ("transcript_entries", "compacted_transcript_entries"): async with conn.execute( - f"UPDATE {table} SET turn_context = ? " + f"UPDATE {table} SET turn_context = CASE " + "WHEN json_type(turn_context, '$.router_model_control_event') = 'text' " + "THEN json_set(CASE " + "WHEN json_type(turn_context, '$.router_model_decision') = 'object' " + "THEN json_set(?, '$.router_model_decision', " + "json_extract(turn_context, '$.router_model_decision')) ELSE ? END, " + "'$.router_model_control_event', " + "json_extract(turn_context, '$.router_model_control_event')) " + "ELSE CASE " + "WHEN json_type(turn_context, '$.router_model_decision') = 'object' " + "THEN json_set(?, '$.router_model_decision', " + "json_extract(turn_context, '$.router_model_decision')) ELSE ? END END " "WHERE session_key = ? AND message_id = ?", - (encoded, session_key, message_id), + (encoded, encoded, encoded, encoded, session_key, message_id), ) as cur: changed += cur.rowcount or 0 return changed > 0 diff --git a/src/opensquilla/squilla_router/lightgbm_model/PROVENANCE.md b/src/opensquilla/squilla_router/lightgbm_model/PROVENANCE.md new file mode 100644 index 0000000000..4c9c9dae9e --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/PROVENANCE.md @@ -0,0 +1,129 @@ +This package contains the exact LightGBM pair selected by the 2026-10-20 model-policy experiment: + +- Model set: `router-lightgbm-99d6ff75-d444135-a1`. +- Original manifest SHA-256: `29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357`. +- Training code identity: `git:d4441356584423be07361495e55b488c95c5db43`. +- Runtime snapshot: `router-training-a34e5ae` beside the experiment's frozen OpenSquilla source. + +`assets/provenance.json` records the upstream source paths and original file +hashes; these identify the source snapshot, not checksums of the local extracts. +`_vendor/` retains only the inference subset. Training, artifact export/loading, +BERT, registry CLI, unused batch transforms, training-only transformer +constructors, and unused encoder inspection/context-manager methods were removed. +The local loader owns the encoder and featurizer directly, without the redundant +feature-runtime wrapper. The unused SQLite embedding cache and its identity +helpers were removed. The memory cache now retains at most 2,048 normalized +embedding rows in least-recently-used order (4 MiB of vector data for this model, +plus keys and container overhead). Cached rows own their storage, and close +releases them. Evicted inputs are recomputed using the same arithmetic. +Tokenization, batching, ordering, and normalization are unchanged. ONNX session +configuration additionally enables `session.x64quantprecision=1` to avoid x64 +U8S8 saturation on CPUs without VNNI; the encoder file is unchanged. On ARM64, +`MatMulIntegerToFloatFusion` and `GeluFusionL2` are disabled to preserve the +unfused operations' numerical behavior; all other optimizations remain enabled. +Other retained function/method definitions preserve the original arithmetic; +imports, module/class descriptions, and type-only annotations/casts were adjusted. +`features/text.py` and `input_contract.py` retain the original extracted definitions. +The original training package declares `LicenseRef-Proprietary`; that declaration +and its original package metadata are retained in `assets/upstream-pyproject.toml`. +The frozen snapshot contains no separate license file. These source files retain +their upstream licensing and are not relicensed by the surrounding project. + +The local adapter removes the registry/database dependency. It verifies the +original manifest and the selected inference closure before native model loading +or pickle deserialization. The two model text files are distributed as gzip; +verification hashes the decompressed original bytes. The encoder, tokenizer, +three joblib transformers, calibration files, bundles, and feature metadata are +unchanged. The packaged model reuses the byte-identical ONNX model and tokenizer +in `../models/v4.2_phase3_inference/bge_onnx/`; the original manifest still +verifies both before loading. External `lightgbm_model_dir` bundles remain +self-contained, with `preprocess/feature/artifacts/bge/model.onnx` and +`preprocess/feature/artifacts/bge/tokenizer.json`. Both layouts avoid the long +content-addressed Windows paths; the source manifests and hashes are unchanged. +Training run manifests and training metrics remain referenced in the original +manifest but are omitted from the inference distribution. + +The 380 float32 features are current-request BGE/PCA64, current-request +character TF-IDF/SVD100, rules48, history BGE/PCA64, route-history16, +previous-answer BGE/PCA64, previous-signals19, and context5. BGE uses CLS pooling, +float32 L2 normalization, a fixed 512-token input, and frozen segment truncation. +An empty history or previous answer produces a zero semantic block. There is no +extra scaling step. Both heads use the original temperature calibration. + +The runtime is CPU-only and uses local files exclusively. Installation needs +numpy, LightGBM, ONNX Runtime, tokenizers, scikit-learn, joblib and pydantic. +Validation used the deployed reference versions: numpy 2.4.4, LightGBM 4.6.0, +ONNX Runtime 1.24.4, tokenizers 0.22.2, scikit-learn 1.8.0, joblib 1.5.3. +The feature-training environment is recorded separately in `feature_recipe.json`. + +`assets/source_parity_cases.json` contains only synthetic test inputs, captured +from the frozen reference runtime. It records raw features, their hashes, both +calibrated output distributions, and the source runner identity. The original +classifier golden vectors are also retained and exercised by the tests. + +The recommended installation preserves the frozen scikit-learn persistence +version (1.8.0) and ONNX Runtime 1.24.4. Compatibility was also checked using +numpy 2.5.3, LightGBM 4.7.0, joblib 1.6.0, and tokenizers 0.23.2: all six +source feature hashes and full-pipeline probability distributions match +exactly on the native AVX-512/VNNI host, and all sixteen original classifier +golden cases agree within 2.23e-16. +`assets/runtime_compatibility.json` records the measured results. ONNX Runtime +1.30.0 changed the code-debug fixture's features by up to 0.006055 and tier +probabilities by up to 0.004872, so it is not interchangeable for this bundle. + +The CI cold-start tier probability discrepancy (maximum 0.019415810857340232) +was reproduced exactly by masking AVX-512/VNNI/AMX capabilities on that host. +The encoder contains dynamically quantized U8S8 matrix multiplications. +[ONNX Runtime documents](https://github.com/microsoft/onnxruntime/blob/v1.24.4/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h#L157-L161) +that the non-VNNI kernels can saturate; its precision option converts the signed +weights to mathematically equivalent unsigned values at session initialization. +With this option, the cold-start features and probabilities match exactly on +AVX2, and all six reference cases match exactly with AVX-512 but VNNI disabled. + +AVX2 differs on `long_three_segments` by up to 0.0033131 in BGE/PCA features, +0.0026307 in intent probabilities, and 0.0064231 in tier probabilities. The other +five inputs match at the original tolerances. Intermediate tensor inspection +located a 1.788e-7 floating-point difference before dynamic quantization: +input/scale is 44.5 on AVX-512 and 44.500019 on AVX2, producing adjacent integers. +This is separate from U8S8 saturation and remains after explicit unsigned-weight +conversion. The measured CPU profiles and first divergent tensor are recorded in +`runtime_compatibility.json`. + +On x86, only the `long_three_segments` full-pipeline reference fixture therefore +uses absolute error bounds of 0.0035 for the three BGE/PCA feature blocks, 0.003 +for intent probabilities, and 0.007 for tier probabilities, modestly above its +measured maxima. Its non-BGE features and all five other fixtures retain their +original feature and probability tolerances. Across all six fixtures, both +classifier labels and the final policy intent, tier, thinking, prompt, reset, +and persisted state must still match the reference exactly. All six inputs retain their labels on the measured native +and AVX2 profiles; this does not guarantee unchanged decisions for arbitrary +requests or unmeasured hardware. The sixteen fixed-feature classifier golden +cases keep the original tight numerical checks (relative 1e-10, absolute 1e-12). +Original golden data, trained assets, and their pinned hashes are unchanged. + +Native Apple M5 ARM64 validation used the same pinned runtime versions above +(without Rosetta). The default fused graph differed from the source by as much +as 0.095450 in tier probabilities. Disabling only +`MatMulIntegerToFloatFusion;GeluFusionL2` reduces that maximum to 0.002824; +four fixtures then match the original feature and probability tolerances. +These are the optimizer names in +[ONNX Runtime 1.24.4](https://github.com/microsoft/onnxruntime/blob/v1.24.4/onnxruntime/core/optimizer/gelu_fusion.h#L20-L27), +and the session's optimizer list is semicolon-delimited. + +On this ARM64 profile, `code_debug` differs by at most 0.0063972 in BGE/PCA +features, 0.0006392 in intent probabilities and 0.0017490 in tier probabilities. +Only ARM64 uses corresponding fixture bounds of 0.0065, 0.00065 and 0.0018. +`long_three_segments` differs by at most 0.0060652 in BGE/PCA features, +0.0014952 in intent probabilities and 0.0028239 in tier probabilities; its +ARM64 semantic bound is 0.0062, with the existing probability bounds unchanged. +All non-BGE features and the other four ARM64 fixtures keep their strict checks. +The full native ARM64 tests require exactly the same classifier labels, final +policy decisions and next states as the frozen source, plus the unchanged +sixteen fixed-feature goldens. These measured fixture bounds are not a general +accuracy guarantee for unseen inputs or other ARM implementations. + +Instrumenting the original graph with basic optimizations reproduces the ARM +`code_debug` rounding boundary: a 1.4902e-8 input difference at layer 1 dynamic +quantization changes input/scale from 9.49999905 to 9.5, producing 151 versus +152. This is the same discrete amplification mechanism observed on AVX2; +`runtime_compatibility.json` records the tensor location and values. diff --git a/src/opensquilla/squilla_router/lightgbm_model/__init__.py b/src/opensquilla/squilla_router/lightgbm_model/__init__.py new file mode 100644 index 0000000000..b4e9f5ead6 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/__init__.py @@ -0,0 +1,23 @@ +"""The hash-pinned LightGBM intent/tier pair used by the model tier policy.""" + +from .model import ( + MANIFEST_HASH, + MODEL_SET_ID, + LightGBMInferenceError, + LightGBMModelError, + LightGBMPrediction, + LightGBMRouterModel, + default_model_root, + validate_artifacts, +) + +__all__ = [ + "MANIFEST_HASH", + "MODEL_SET_ID", + "LightGBMInferenceError", + "LightGBMModelError", + "LightGBMPrediction", + "LightGBMRouterModel", + "default_model_root", + "validate_artifacts", +] diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/__init__.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/__init__.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/lightgbm380.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/lightgbm380.py new file mode 100644 index 0000000000..5f86e30a67 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/lightgbm380.py @@ -0,0 +1,138 @@ +"""Frozen LightGBM 380-dimensional feature inference pipeline. + +This module creates features only. It intentionally does not import LightGBM. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Protocol + +import numpy as np + +from .numeric import extract_numeric88 +from .registry import NUMERIC88_REGISTRY +from .text import SEGMENT_KINDS, SegmentKind, TextSegments, extract_text_segments + + +SEMANTIC_DIM = 64 +CHARACTER_DIM = 100 +FEATURE_DIM = 380 + + +class EmbeddingEncoder(Protocol): + def encode( + self, + texts: Sequence[str], + *, + segment_kinds: Sequence[SegmentKind], + ) -> object: ... + + +class Projector(Protocol): + def transform(self, values: object) -> object: ... + + +class TextVectorizer(Protocol): + def transform(self, texts: Sequence[str]) -> object: ... + + +def _numeric_names(start: int, stop: int) -> tuple[str, ...]: + return tuple(feature.name for feature in NUMERIC88_REGISTRY[start:stop]) + + +FEATURE_NAMES_380 = ( + tuple(f"current.semantic.{index:03d}" for index in range(SEMANTIC_DIM)) + + tuple(f"current.character.{index:03d}" for index in range(CHARACTER_DIM)) + + _numeric_names(0, 48) + + tuple(f"history.semantic.{index:03d}" for index in range(SEMANTIC_DIM)) + + _numeric_names(48, 64) + + tuple(f"previous.semantic.{index:03d}" for index in range(SEMANTIC_DIM)) + + _numeric_names(64, 83) + + _numeric_names(83, 88) +) + +if len(FEATURE_NAMES_380) != FEATURE_DIM: + raise AssertionError(f"feature registry has {len(FEATURE_NAMES_380)} entries") + + +def _matrix(values: object, *, rows: int | None = None, columns: int | None = None) -> np.ndarray: + matrix = np.asarray(values, dtype=np.float32) + if matrix.ndim != 2: + raise ValueError(f"transformer output must be 2-D, got shape {matrix.shape}") + if rows is not None and matrix.shape[0] != rows: + raise ValueError(f"transformer output has {matrix.shape[0]} rows, expected {rows}") + if columns is not None and matrix.shape[1] != columns: + raise ValueError(f"transformer output has {matrix.shape[1]} columns, expected {columns}") + if not np.isfinite(matrix).all(): + raise ValueError("transformer output contains non-finite values") + return matrix + + +class LightGBM380Featurizer: + """Transform canonical rows with the frozen PCA/TF-IDF/SVD artifacts.""" + + def __init__( + self, + *, + embedder: EmbeddingEncoder, + semantic_projector: Projector, + text_vectorizer: TextVectorizer, + character_projector: Projector, + ) -> None: + self._embedder = embedder + self._semantic_projector = semantic_projector + self._text_vectorizer = text_vectorizer + self._character_projector = character_projector + self._fitted = False + + def _semantic_blocks(self, segments: TextSegments) -> np.ndarray: + texts = segments.as_tuple() + result = np.zeros((3, SEMANTIC_DIM), dtype=np.float32) + positions = [index for index, text in enumerate(texts) if text] + if positions: + embeddings = _matrix( + self._embedder.encode( + [texts[index] for index in positions], + segment_kinds=[SEGMENT_KINDS[index] for index in positions], + ), + rows=len(positions), + ) + projected = _matrix( + self._semantic_projector.transform(embeddings), + rows=len(positions), + columns=SEMANTIC_DIM, + ) + result[positions] = projected + return result + + def transform_row(self, row: Mapping[str, object]) -> np.ndarray: + if not self._fitted: + raise RuntimeError("call fit(train_rows) before transform_row") + segments = extract_text_segments(row) + semantics = self._semantic_blocks(segments) + character = _matrix( + self._character_projector.transform( + self._text_vectorizer.transform([segments.current_request]) + ), + rows=1, + columns=CHARACTER_DIM, + )[0] + numeric = extract_numeric88(row) + result = np.concatenate( + ( + semantics[0], + character, + numeric[0:48], + semantics[1], + numeric[48:64], + semantics[2], + numeric[64:83], + numeric[83:88], + ) + ).astype(np.float32) + if result.shape != (FEATURE_DIM,): + raise AssertionError(f"feature vector shape drifted to {result.shape}") + if not np.isfinite(result).all(): + raise ValueError("feature vector contains non-finite values") + return result diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/numeric.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/numeric.py new file mode 100644 index 0000000000..be4abb5453 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/numeric.py @@ -0,0 +1,348 @@ +"""Deterministic numeric88 extraction with no model-runtime dependency.""" + +from __future__ import annotations + +import math +from typing import Any, cast +import re +from collections import Counter +from collections.abc import Mapping, Sequence + +import numpy as np + +from .registry import NUMERIC88_REGISTRY +from .text import canonicalize_text, extract_text_segments, sample_input + + +_CODE_BLOCK_RE = re.compile(r"```[\s\S]*?```") +_JSON_RE = re.compile(r"\{[\s\S]*?[\"'][\w]+[\"']\s*:") +_YAML_RE = re.compile(r"^[\w_]+:\s+\S", re.MULTILINE) +_CSV_RE = re.compile(r"^[^,\n]+,[^,\n]+,[^,\n]+", re.MULTILINE) +_TABLE_RE = re.compile(r"\|.*\|.*\|") +_FILE_PATH_RE = re.compile( + r"(?:^|[\s\"'`(])([a-zA-Z_][\w.-]*/[\w./-]+\.[\w]+)", re.MULTILINE +) +_URL_RE = re.compile(r"https?://\S+") +_LOG_RE = re.compile( + r"(\d{4}[-/]\d{2}[-/]\d{2}[\sT]\d{2}:\d{2}.*\n){3,}" + r"|(^\[?(INFO|WARN|ERROR|DEBUG)\]?\s.*\n){3,}", + re.MULTILINE, +) +_SHELL_RE = re.compile(r"^\$\s+\w|^>\s+\w|```(?:bash|sh|shell)", re.MULTILINE) +_TRACEBACK_RE = re.compile(r"Traceback \(most recent|stderr:|\.py\", line \d+") +_BULLET_RE = re.compile(r"^[\s]*[-*]\s", re.MULTILINE) +_NUMBERED_RE = re.compile(r"^[\s]*\d+[.)、]\s", re.MULTILINE) +_INLINE_CODE_RE = re.compile(r"`[^`]{4,}`") +_STEPS_RE = re.compile(r"^\s*\d+[.、]\s", re.MULTILINE) + +_KEYWORDS = { + "debug": ("error", "bug", "exception", "traceback", "failed", "root cause", "报错", "根因", "修复", "stack trace", "debug"), + "research": ("调研", "research", "对比", "compare", "survey", "分析报告", "competitive analysis", "综述"), + "architecture": ("architecture", "架构", "重构", "refactor", "monorepo", "codebase", "module", "dependency"), + "compare": ("对比", "compare", "audit", "审计", "review", "评估"), + "planning": ("plan", "规划", "roadmap", "设计方案", "workflow", "pipeline", "步骤", "step by step"), + "strict": ("json", "yaml", "csv", "schema", "只返回", "不要解释", "按格式", "only return", "no explanation"), + "risk": ("deploy", "rollback", "migration", "delete", "overwrite", "production", "生产", "部署", "删除", "客户", "法务", "财务"), + "production": ("production", "生产", "prod", "线上", "正式环境"), + "customer": ("customer", "客户", "用户邮件", "client"), + "delete": ("delete", "remove", "drop", "truncate", "删除", "清空", "覆盖", "overwrite"), + "formal": ("formal", "正式", "official", "公文", "合同", "法律"), + "constraint": ("必须", "不能", "不要", "只能", "must", "shall", "required", "forbidden", "不允许", "至少", "最多"), + "teaching": ("how does", "explain", "what is", "why does", "how to", "教我", "解释", "为什么", "怎么", "是什么", "how can", "tell me about", "walk me through", "介绍", "说明"), + "implementation": ("implement", "write function", "write a", "create a", "写个", "实现", "用法", "帮我写", "生成代码", "add a", "build a", "make a", "写一个", "编写"), +} + +_CLARIFY_RE = re.compile(r"(?:能否|请\s*提供|需要(?:更多|具体).{0,8}信息|could you (?:clarify|provide)|please (?:specify|provide)|clarify which)", re.I) +_REFUSAL_RE = re.compile(r"(?:I cannot|I can't help|对不起.{0,5}无法|抱歉.{0,5}不能|作为(?:AI|大语言模型))", re.I) +_SELF_DOUBT_RE = re.compile(r"(?:我不(?:确定|清楚)|可能(?:不太|不一定)|not sure|might not be|I'm not entirely)", re.I) +_CONTINUE_RE = re.compile(r"(?:请继续|继续|接着|续写|展开一下|再说|more|continue|go on|carry on|next)", re.I) +_REASONING_RE = re.compile(r"(?:why|compare|trade[ -]?off|analy[sz]e|architecture|reasoning|design|解释|原因|对比|分析|架构|设计|权衡)", re.I) + +_TIER_INDEX = {"C0": 0, "C1": 1, "C2": 2, "C3": 3, "R0": 0, "R1": 1, "R2": 2, "R3": 3} +_TRAJECTORIES = ( + "cold_start", + "stable_low", + "stable_high", + "escalating", + "descalating", + "oscillating", + "unclear", + "mixed", +) + + +def _keyword_count(text: str, name: str) -> float: + lowered = text.lower() + return float(sum(keyword.lower() in lowered for keyword in _KEYWORDS[name])) + + +def _char_ratios(text: str) -> tuple[float, float, float]: + if not text: + return 0.0, 0.0, 0.0 + size = len(text) + zh = sum("\u4e00" <= char <= "\u9fff" for char in text) + latin = sum(char.isascii() and char.isalpha() for char in text) + code = sum(char in "{}[]();=<>|&!@#$%^*~`\\" for char in text) + return zh / size, latin / size, code / size + + +def _as_nonnegative(value: object) -> float: + if isinstance(value, bool): + return 0.0 + try: + number = float(cast(Any, value or 0.0)) + except (TypeError, ValueError): + return 0.0 + return number if math.isfinite(number) and number > 0 else 0.0 + + +def _log_usage(usage: Mapping[str, object], key: str) -> float: + return math.log1p(_as_nonnegative(usage.get(key))) / 10.0 + + +def extract_rules48(current_request: str) -> np.ndarray: + text = canonicalize_text(current_request) + words = text.split() + lines = text.split("\n") + zh, latin, code = _char_ratios(text) + code_blocks = _CODE_BLOCK_RE.findall(text) + file_refs = set(_FILE_PATH_RE.findall(text)) + keyword_values = {name: _keyword_count(text, name) for name in _KEYWORDS} + quoted = re.findall(r"[\"'`](.*?)[\"'`]", text) + diversity = len({word.lower() for word in words}) / max(len(words), 1) + total_core_keywords = sum(keyword_values[name] for name in ("debug", "research", "architecture", "compare", "planning", "strict", "risk")) + values = ( + len(text), + len(words), + len(lines), + len(text) / max(len(lines), 1), + zh, + latin, + code, + float(zh > 0.1 and latin > 0.1), + float(bool(code_blocks)), + len(code_blocks), + sum(len(block) for block in code_blocks), + float(bool(_JSON_RE.search(text))), + float(bool(_YAML_RE.search(text))), + float(bool(_CSV_RE.search(text))), + float(bool(_TABLE_RE.search(text))), + text.count("?") + text.count("?"), + text.count("!") + text.count("!"), + len(_BULLET_RE.findall(text)), + len(_NUMBERED_RE.findall(text)), + keyword_values["debug"], + keyword_values["research"], + keyword_values["architecture"], + keyword_values["compare"], + keyword_values["planning"], + keyword_values["strict"], + keyword_values["risk"], + keyword_values["production"], + keyword_values["customer"], + keyword_values["delete"], + keyword_values["formal"], + float(bool(file_refs)), + float(bool(_URL_RE.search(text))), + float(bool(_LOG_RE.search(text))), + float(bool(_SHELL_RE.search(text))), + float(bool(_TRACEBACK_RE.search(text))), + keyword_values["constraint"], + sum(len(value) for value in quoted) / max(len(text), 1), + diversity, + keyword_values["teaching"], + keyword_values["implementation"], + float(len(file_refs) == 0), + float(1 <= len(file_refs) <= 2), + float(len(file_refs) >= 3), + float(bool(code_blocks) and keyword_values["debug"] == 0), + float(len(text) < 200), + float(200 <= len(text) <= 1000), + float(len(text) > 1000), + float(total_core_keywords < 2), + ) + result = np.asarray(values, dtype=np.float32) + if result.shape != (48,): + raise AssertionError(f"rules48 shape drifted to {result.shape}") + return result + + +def _route_index(entry: Mapping[str, object]) -> int: + value = entry.get("tier_id") or entry.get("tier") or entry.get("route_class") + normalized = str(value).upper() + if normalized not in _TIER_INDEX: + raise ValueError(f"route history entry has invalid tier: {value!r}") + return _TIER_INDEX[normalized] + + +def _finite_number(value: object, default: float = 0.0) -> float: + if isinstance(value, bool): + return default + try: + number = float(cast(Any, value)) + except (TypeError, ValueError): + return default + return number if math.isfinite(number) else default + + +def _trajectory(indices: list[int], difficulties: list[float]) -> str: + if not indices: + return "cold_start" + if len(indices) < 2: + return "unclear" + if all(index <= 1 for index in indices): + return "stable_low" + if all(index >= 2 for index in indices): + return "stable_high" + signs = [] + for before, after in zip(difficulties, difficulties[1:]): + delta = after - before + signs.append(1 if delta > 0.3 else (-1 if delta < -0.3 else 0)) + nonzero = [sign for sign in signs if sign] + if len(nonzero) >= 2 and all(sign == 1 for sign in nonzero): + return "escalating" + if len(nonzero) >= 2 and all(sign == -1 for sign in nonzero): + return "descalating" + changes = sum(left != right for left, right in zip(nonzero, nonzero[1:])) + return "oscillating" if changes >= 2 else "mixed" + + +def extract_route_history16(route_history: object) -> np.ndarray: + if route_history is None: + entries: list[Mapping[str, object]] = [] + else: + if isinstance(route_history, (str, bytes)) or not isinstance(route_history, Sequence): + raise TypeError("input.route_history must be a sequence") + entries = [] + for value in route_history[-5:]: + if not isinstance(value, Mapping): + raise TypeError("each route_history entry must be a mapping") + entries.append(value) + indices = [_route_index(entry) for entry in entries] + difficulties = [_finite_number(entry.get("difficulty", entry.get("difficulty_score", 0.0))) for entry in entries] + margins = [_finite_number(entry.get("margin", 0.0)) for entry in entries] + result = np.zeros(16, dtype=np.float32) + if entries: + result[0] = indices[-1] + result[1] = difficulties[-1] + result[2] = margins[-1] + result[3] = max(indices) + result[4] = len(entries) + result[5] = len(entries) + counts = Counter(indices) + top_count = max(counts.values()) + result[6] = max(index for index, count in counts.items() if count == top_count) + result[7] = sum(left != right for left, right in zip(indices, indices[1:])) + else: + result[[0, 3, 6]] = -1.0 + result[8 + _TRAJECTORIES.index(_trajectory(indices, difficulties))] = 1.0 + return result + + +def extract_previous_signals19( + current_request: str, + previous_answer: object, + previous_usage: object, +) -> np.ndarray: + current = canonicalize_text(current_request) + previous = canonicalize_text(previous_answer) + if previous_usage is None: + usage: Mapping[str, object] = {} + elif isinstance(previous_usage, Mapping): + usage = previous_usage + else: + raise TypeError("input.previous_usage must be a mapping or null") + + assistant = np.zeros(12, dtype=np.float32) + if previous: + input_tokens = max(_as_nonnegative(usage.get("input_tokens")), 1.0) + assistant[:] = ( + 1.0, + float(bool(_CLARIFY_RE.search(previous))), + float(bool(_REFUSAL_RE.search(previous))), + float(bool(_SELF_DOUBT_RE.search(previous))), + float("```" in previous or bool(_INLINE_CODE_RE.search(previous))), + float(bool(_STEPS_RE.search(previous))), + _log_usage(usage, "output_tokens"), + _log_usage(usage, "reasoning_tokens"), + _log_usage(usage, "duration_ms"), + min(len(previous) / max(len(current), 1), 5.0) / 5.0, + _char_ratios(previous)[0], + min(_as_nonnegative(usage.get("cached_tokens")) / input_tokens, 1.0), + ) + stripped = current.strip() + continuation = np.asarray( + ( + float(bool(stripped) and len(stripped) <= 24 and bool(_CONTINUE_RE.search(stripped))), + _log_usage(usage, "output_tokens"), + ), + dtype=np.float32, + ) + qmarks = current.count("?") + current.count("?") + reasoning = np.asarray( + ( + float(bool(_REASONING_RE.search(current))), + min(qmarks / max(len(current), 1) * 20.0, 1.0), + math.log1p(len(current)) / 10.0, + _log_usage(usage, "reasoning_tokens"), + _log_usage(usage, "duration_ms"), + ), + dtype=np.float32, + ) + result = np.concatenate((assistant, continuation, reasoning)).astype(np.float32) + if result.shape != (19,): + raise AssertionError(f"previous_signals19 shape drifted to {result.shape}") + return result + + +def extract_context5(row: Mapping[str, object]) -> np.ndarray: + payload = sample_input(row) + segments = extract_text_segments(row) + route_history = payload.get("route_history") or [] + if isinstance(route_history, (str, bytes)) or not isinstance(route_history, Sequence): + raise TypeError("input.route_history must be a sequence") + turn_index = min(len(route_history[-5:]), 20) + raw_context = payload.get("context") or {} + if not isinstance(raw_context, Mapping): + raise TypeError("input.context must be a mapping or null") + estimated = raw_context.get("context_tokens_est") + if estimated is None: + history_chars = sum(len(canonicalize_text(value)) for value in cast(Sequence[object], payload.get("history_user") or [])) + estimated_tokens = ( + len(segments.current_request) + history_chars + len(segments.previous_answer) + ) // 4 + else: + estimated_tokens = int(max(_finite_number(estimated), 0.0)) + result = np.asarray( + ( + turn_index / 20.0, + min(estimated_tokens, 20_000) / 20_000.0, + float("```" in segments.current_request), + float(turn_index >= 4), + float(estimated_tokens > 2_000), + ), + dtype=np.float32, + ) + return result + + +def extract_numeric88(row: Mapping[str, object]) -> np.ndarray: + payload = sample_input(row) + segments = extract_text_segments(row) + result = np.concatenate( + ( + extract_rules48(segments.current_request), + extract_route_history16(payload.get("route_history")), + extract_previous_signals19( + segments.current_request, + payload.get("previous_answer"), + payload.get("previous_usage"), + ), + extract_context5(row), + ) + ).astype(np.float32) + if result.shape != (len(NUMERIC88_REGISTRY),): + raise AssertionError(f"numeric88 shape drifted to {result.shape}") + if not np.isfinite(result).all(): + raise ValueError("numeric88 contains a non-finite value") + return result diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/registry.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/registry.py new file mode 100644 index 0000000000..a46dd59b9a --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/registry.py @@ -0,0 +1,182 @@ +"""The immutable ``numeric88.v1`` feature registry. + +The registry is executable metadata: extraction tests use it to verify that +names, offsets, defaults, and block boundaries cannot drift independently. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +SCHEMA_VERSION = "numeric88.registry.v1" +FEATURE_VERSION = "numeric88.v1" +DTYPE = "float32" + + +@dataclass(frozen=True, slots=True) +class NumericFeature: + name: str + offset: int + dtype: str + missing: str + default: float + source: str + block: str + + +RULE_NAMES = ( + "char_count", + "word_count", + "line_count", + "avg_chars_per_line", + "zh_char_ratio", + "latin_char_ratio", + "code_char_ratio", + "mixed_zh_en", + "has_code_block", + "code_block_count", + "code_block_chars", + "has_json", + "has_yaml", + "has_csv", + "has_table", + "question_mark_count", + "exclamation_mark_count", + "bullet_count", + "numbered_list_count", + "debug_keyword_count", + "research_keyword_count", + "architecture_keyword_count", + "compare_keyword_count", + "planning_keyword_count", + "strict_format_keyword_count", + "high_risk_keyword_count", + "production_keyword_count", + "customer_keyword_count", + "delete_keyword_count", + "formal_keyword_count", + "has_file_path", + "has_url", + "has_log", + "has_shell", + "has_traceback", + "constraint_keyword_count", + "quoted_text_ratio", + "word_diversity", + "teaching_keyword_count", + "implementation_keyword_count", + "file_count_zero", + "file_count_one_or_two", + "file_count_three_plus", + "code_without_debug", + "length_under_200", + "length_200_to_1000", + "length_over_1000", + "low_keyword_density", +) + +ROUTE_HISTORY_NAMES = ( + "previous_tier_index", + "previous_difficulty", + "previous_margin", + "highest_tier_index", + "turn_index", + "route_history_length", + "dominant_tier_index", + "route_switch_count", + "trajectory_cold_start", + "trajectory_stable_low", + "trajectory_stable_high", + "trajectory_escalating", + "trajectory_descalating", + "trajectory_oscillating", + "trajectory_unclear", + "trajectory_mixed", +) + +PREVIOUS_SIGNAL_NAMES = ( + "has_previous_answer", + "previous_answer_clarifies", + "previous_answer_refuses", + "previous_answer_self_doubt", + "previous_answer_has_code", + "previous_answer_has_steps", + "previous_output_tokens_log", + "previous_reasoning_tokens_log", + "previous_duration_log", + "answer_to_request_length_ratio", + "previous_answer_zh_ratio", + "previous_cache_ratio", + "short_continue_cue", + "previous_output_tokens_log_repeat", + "reasoning_cue", + "question_density", + "request_length_log", + "previous_reasoning_tokens_log_repeat", + "previous_duration_log_repeat", +) + +CONTEXT_NAMES = ( + "turn_index_normalized", + "context_tokens_normalized", + "has_code_block", + "is_deep_conversation", + "is_heavy_context", +) + + +def _make_registry() -> tuple[NumericFeature, ...]: + features: list[NumericFeature] = [] + + def add_block( + block: str, + names: tuple[str, ...], + source: str, + *, + defaults: dict[str, float] | None = None, + missing: str = "zero", + ) -> None: + block_defaults = defaults or {} + for short_name in names: + features.append( + NumericFeature( + name=f"{block}.{short_name}", + offset=len(features), + dtype=DTYPE, + missing=missing, + default=float(block_defaults.get(short_name, 0.0)), + source=source, + block=block, + ) + ) + + add_block("current.rule", RULE_NAMES, "input.current_request") + add_block( + "history.route", + ROUTE_HISTORY_NAMES, + "input.route_history", + defaults={ + "previous_tier_index": -1.0, + "highest_tier_index": -1.0, + "dominant_tier_index": -1.0, + "trajectory_cold_start": 1.0, + }, + missing="documented_default", + ) + add_block( + "previous.signal", + PREVIOUS_SIGNAL_NAMES, + "input.previous_answer + input.previous_usage + input.current_request", + ) + add_block( + "context", + CONTEXT_NAMES, + "input.current_request + input.history_user + input.previous_answer + input.route_history + input.context", + ) + if len(features) != 88: + raise AssertionError(f"numeric88 registry has {len(features)} entries") + return tuple(features) + + +NUMERIC88_REGISTRY = _make_registry() diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/runtime.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/runtime.py new file mode 100644 index 0000000000..e792a6ef12 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/runtime.py @@ -0,0 +1,373 @@ +"""Frozen BGE and LightGBM feature inference definitions; see ../../PROVENANCE.md.""" + +from __future__ import annotations + +import os +import platform +from collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor +from collections.abc import Mapping, Sequence +from hashlib import sha256 +from pathlib import Path +from typing import Any + +import numpy as np + +from .text import SegmentKind, truncate_content_tokens + + +PCA_DIM = 64 +SVD_DIM = 100 +BGE_MAX_CONTENT_TOKENS = 510 +_BGE_CACHE_WRITE_BATCH = 256 +_BGE_MEMORY_CACHE_MAX_ENTRIES = 2048 + + +def _dense_2d(values: object, *, name: str) -> np.ndarray: + result = np.asarray(values, dtype=np.float32) + if result.ndim != 2: + raise ValueError(f"{name} must be a 2-D matrix, got {result.shape}") + if result.shape[0] == 0 or result.shape[1] == 0: + raise ValueError(f"{name} must not be empty") + if not np.isfinite(result).all(): + raise ValueError(f"{name} contains non-finite values") + return result + + +def _pad_columns(values: object, width: int, *, name: str) -> np.ndarray: + result = np.asarray(values, dtype=np.float32) + if result.ndim != 2 or result.shape[1] > width: + raise ValueError(f"{name} has invalid shape {result.shape}; expected at most {width} columns") + if not np.isfinite(result).all(): + raise ValueError(f"{name} contains non-finite values") + if result.shape[1] == width: + return result + padded = np.zeros((result.shape[0], width), dtype=np.float32) + padded[:, : result.shape[1]] = result + return padded + + +class SklearnPCA64Projector: + """PCA fitted on train embeddings, with a fixed 64-column output.""" + + output_dim = PCA_DIM + estimator: Any # Restored from the frozen joblib state. + + def transform(self, values: object) -> np.ndarray: + if self.estimator is None: + raise RuntimeError("PCA projector is not fitted") + matrix = _dense_2d(values, name="PCA input") + return _pad_columns(self.estimator.transform(matrix), PCA_DIM, name="PCA output") + +class SklearnCharTfidfVectorizer: + """Frozen character ``char_wb`` TF-IDF configuration.""" + + estimator: Any # Restored from the frozen joblib state. + + def transform(self, texts: Sequence[str]) -> object: + return self.estimator.transform(list(texts)) + +class SklearnSVD100Projector: + """TruncatedSVD fitted on train TF-IDF, with a fixed 100-column output.""" + + output_dim = SVD_DIM + estimator: Any # Restored from the frozen joblib state. + + def transform(self, values: object) -> np.ndarray: + if self.estimator is None: + raise RuntimeError("SVD projector is not fitted") + return _pad_columns(self.estimator.transform(values), SVD_DIM, name="SVD output") + +def _tokenizer_content_ids(tokenizer: object, text: str) -> list[int]: + encode = getattr(tokenizer, "encode", None) + if not callable(encode): + raise TypeError("tokenizer must provide encode(text, add_special_tokens=False)") + try: + encoded = encode(text, add_special_tokens=False) + except TypeError: + encoded = encode(text) + values = getattr(encoded, "ids", encoded) + if isinstance(values, np.ndarray): + values = values.tolist() + if isinstance(values, (str, bytes)) or not isinstance(values, Sequence): + raise TypeError("tokenizer encode result must contain a sequence of token IDs") + return [int(value) for value in values] + + +def _special_token_id(tokenizer: object, name: str, token: str) -> int: + attribute = getattr(tokenizer, f"{name}_token_id", None) + if attribute is not None: + return int(attribute) + token_to_id = getattr(tokenizer, "token_to_id", None) + if callable(token_to_id): + value = token_to_id(token) + if value is not None: + return int(value) + convert = getattr(tokenizer, "convert_tokens_to_ids", None) + if callable(convert): + value = convert(token) + if value is not None and int(value) >= 0: + return int(value) + raise ValueError(f"tokenizer does not define {token!r}") + + +def _local_model_file(model_dir: Path, explicit: str | Path | None, candidates: Sequence[str]) -> Path: + if explicit is not None: + path = Path(explicit) + path = path if path.is_absolute() else model_dir / path + if not path.is_file(): + raise FileNotFoundError(path) + resolved = path.resolve() + if resolved != model_dir and model_dir not in resolved.parents: + raise ValueError("BGE files must stay inside the local model directory") + return resolved + for relative in candidates: + path = model_dir / relative + if path.is_file(): + return path.resolve() + raise FileNotFoundError(f"none of {list(candidates)} exists under {model_dir}") + + +class BGEOnnxEncoder: + """Local-only BGE ONNX encoder using CLS pooling and L2 normalization.""" + + def __init__( + self, + model_dir: str | Path, + *, + tokenizer_path: str | Path | None = None, + model_path: str | Path | None = None, + tokenizer: object | None = None, + session: object | None = None, + providers: Sequence[str] = ("CPUExecutionProvider",), + max_content_tokens: int = BGE_MAX_CONTENT_TOKENS, + batch_size: int = 1, + workers: int = 1, + ) -> None: + self.model_dir = Path(model_dir).expanduser().resolve() + if not self.model_dir.is_dir(): + raise FileNotFoundError(self.model_dir) + self.tokenizer_path = _local_model_file( + self.model_dir, tokenizer_path, ("tokenizer.json",) + ) + self.model_path = _local_model_file( + self.model_dir, model_path, ("model.onnx", "onnx/model.onnx") + ) + if max_content_tokens != BGE_MAX_CONTENT_TOKENS: + raise ValueError("BGE feature contract requires exactly 510 content tokens") + if batch_size != 1: + raise ValueError( + "BGE feature contract fixes batch_size=1 for offline/online parity" + ) + if workers < 1: + raise ValueError("BGE workers must be a positive integer") + self.max_content_tokens = max_content_tokens + self.batch_size = int(batch_size) + self.workers = int(workers) + + if tokenizer is None: + try: + from tokenizers import Tokenizer + except ImportError as exc: # pragma: no cover - optional runtime dependency + raise RuntimeError("tokenizers is required to load local tokenizer.json") from exc + tokenizer = Tokenizer.from_file(str(self.tokenizer_path)) + if session is None: + try: + import onnxruntime as ort + except ImportError as exc: # pragma: no cover - optional runtime dependency + raise RuntimeError("onnxruntime is required to run the local BGE model") from exc + try: + intra_threads = int(os.environ.get("ROUTER_ORT_INTRA_OP_THREADS", "1")) + inter_threads = int(os.environ.get("ROUTER_ORT_INTER_OP_THREADS", "1")) + except ValueError as exc: + raise RuntimeError("ORT thread settings must be positive integers") from exc + if intra_threads < 1 or inter_threads < 1: + raise RuntimeError("ORT thread settings must be positive integers") + options = ort.SessionOptions() + options.intra_op_num_threads = intra_threads + options.inter_op_num_threads = inter_threads + options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL + options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL + # U8S8 kernels saturate on x64 without VNNI; ORT's precision mode + # avoids that integer overflow without changing the frozen model bytes. + options.add_session_config_entry("session.x64quantprecision", "1") + if platform.machine().lower() in {"arm64", "aarch64"}: + # ARM fused arithmetic amplifies dynamic-quantization rounding. + # Keep the original operations; see runtime_compatibility.json. + options.add_session_config_entry( + "optimization.disable_specified_optimizers", + "MatMulIntegerToFloatFusion;GeluFusionL2", + ) + session = ort.InferenceSession( + str(self.model_path), + sess_options=options, + providers=list(providers), + ) + self.tokenizer = tokenizer + self.session: Any = session + self.cls_token_id = _special_token_id(tokenizer, "cls", "[CLS]") + self.sep_token_id = _special_token_id(tokenizer, "sep", "[SEP]") + self.pad_token_id = _special_token_id(tokenizer, "pad", "[PAD]") + self._memory_cache: OrderedDict[str, np.ndarray] = OrderedDict() + self._cache_hits = 0 + self._cache_misses = 0 + self._closed = False + + def close(self) -> None: + if self._closed: + return + self._closed = True + self._memory_cache.clear() + + def _input_specs(self) -> dict[str, object]: + get_inputs = getattr(self.session, "get_inputs", None) + if not callable(get_inputs): + raise TypeError("ONNX session must provide get_inputs()") + return {item.name: item for item in get_inputs()} + + @staticmethod + def _sequence_cache_key(sequence: Sequence[int]) -> str: + values = np.asarray(sequence, dtype=" np.ndarray: + if width < len(sequence): + raise ValueError( + "ONNX input sequence width is smaller than the 512-token contract" + ) + input_ids = np.full((1, width), self.pad_token_id, dtype=np.int64) + attention_mask = np.zeros_like(input_ids) + input_ids[0, : len(sequence)] = sequence + attention_mask[0, : len(sequence)] = 1 + available = { + "input_ids": input_ids, + "attention_mask": attention_mask, + "token_type_ids": np.zeros_like(input_ids), + } + feed = {name: available[name] for name in specs if name in available} + outputs = self.session.run(None, feed) + if not outputs: + raise ValueError("ONNX BGE model returned no outputs") + hidden = np.asarray(outputs[0], dtype=np.float32) + if hidden.ndim == 3: + pooled = hidden[0, 0, :] + elif hidden.ndim == 2: + pooled = hidden[0, :] + else: + raise ValueError(f"unexpected BGE output shape: {hidden.shape}") + if pooled.ndim != 1 or not np.isfinite(pooled).all(): + raise ValueError("ONNX BGE model returned an invalid pooled row") + return pooled + + def encode( + self, + texts: Sequence[str], + *, + segment_kinds: Sequence[SegmentKind], + ) -> np.ndarray: + batch = list(texts) + kinds = list(segment_kinds) + if self._closed: + raise RuntimeError("BGE encoder is closed") + if not batch or len(batch) != len(kinds): + raise ValueError("texts and segment_kinds must be non-empty and have equal length") + if any(not isinstance(text, str) or not text for text in batch): + raise ValueError("empty semantic segments must bypass the BGE encoder") + + specs = self._input_specs() + if "input_ids" not in specs: + raise ValueError("ONNX model must expose an input_ids input") + fixed_length: int | None = None + shape = getattr(specs["input_ids"], "shape", None) + if isinstance(shape, Sequence) and len(shape) > 1 and isinstance(shape[1], int): + fixed_length = shape[1] + # Every ONNX call retains the frozen single-row, 512-token shape. Work + # may run concurrently, but rows are collected in input order. + width = fixed_length or (BGE_MAX_CONTENT_TOKENS + 2) + results: list[np.ndarray | None] = [None] * len(batch) + pending: dict[str, tuple[list[int], list[int]]] = {} + for index, (text, kind) in enumerate(zip(batch, kinds, strict=True)): + content = truncate_content_tokens( + kind, + _tokenizer_content_ids(self.tokenizer, text), + max_content_tokens=self.max_content_tokens, + ) + sequence = [self.cls_token_id, *content, self.sep_token_id] + cache_key = self._sequence_cache_key(sequence) + cached = self._memory_cache.get(cache_key) + if cached is not None: + self._memory_cache.move_to_end(cache_key) + results[index] = cached + self._cache_hits += 1 + continue + existing = pending.get(cache_key) + if existing is None: + pending[cache_key] = (sequence, [index]) + else: + existing[1].append(index) + + misses = list(pending.items()) + executor = ( + ThreadPoolExecutor(max_workers=self.workers, thread_name_prefix="bge-onnx") + if self.workers > 1 + else None + ) + try: + for start in range(0, len(misses), _BGE_CACHE_WRITE_BATCH): + items = misses[start : start + _BGE_CACHE_WRITE_BATCH] + if executor is None: + raw_rows = [ + self._run_sequence(sequence, specs=specs, width=width) + for _, (sequence, _) in items + ] + else: + futures = [ + executor.submit( + self._run_sequence, + sequence, + specs=specs, + width=width, + ) + for _, (sequence, _) in items + ] + raw_rows = [future.result() for future in futures] + pooled = np.stack(raw_rows).astype(np.float32, copy=False) + if not np.isfinite(pooled).all(): + raise ValueError("BGE output has invalid rows or non-finite values") + norms = np.linalg.norm(pooled, axis=1, keepdims=True) + normalized = np.divide( + pooled, + norms, + out=np.zeros_like(pooled), + where=norms > 0, + ).astype(np.float32, copy=False) + for item_index, (cache_key, (_, indices)) in enumerate(items): + # Own only this row; a view could retain an entire batch + # after all its other cache entries have been evicted. + value = normalized[item_index].copy() + self._memory_cache[cache_key] = value + while len(self._memory_cache) > _BGE_MEMORY_CACHE_MAX_ENTRIES: + self._memory_cache.popitem(last=False) + for index in indices: + results[index] = value + self._cache_hits += sum(len(indices) - 1 for _, (_, indices) in items) + self._cache_misses += len(items) + finally: + if executor is not None: + executor.shutdown(wait=True, cancel_futures=True) + + if any(value is None for value in results): + raise AssertionError("BGE cache failed to materialize every input row") + normalized = np.stack([value for value in results if value is not None]) + if normalized.shape[0] != len(batch) or not np.isfinite(normalized).all(): + raise ValueError("BGE output has invalid rows or non-finite values") + return normalized.astype(np.float32, copy=False) diff --git a/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/text.py b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/text.py new file mode 100644 index 0000000000..78c01de602 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/_vendor/features/text.py @@ -0,0 +1,91 @@ +"""Canonical three-segment text extraction shared by offline and online code.""" + +from __future__ import annotations + +import unicodedata +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from typing import Literal, TypeVar + + +HISTORY_MAX_TURNS = 4 +HISTORY_SEPARATOR = "\n[TURN]\n" +SegmentKind = Literal["current_request", "recent_history", "previous_answer"] +SEGMENT_KINDS: tuple[SegmentKind, SegmentKind, SegmentKind] = ( + "current_request", + "recent_history", + "previous_answer", +) +TokenT = TypeVar("TokenT") + + +@dataclass(frozen=True, slots=True) +class TextSegments: + current_request: str + recent_history: str + previous_answer: str + + def as_tuple(self) -> tuple[str, str, str]: + return self.current_request, self.recent_history, self.previous_answer + + +def canonicalize_text(value: object) -> str: + if value is None: + return "" + if not isinstance(value, str): + raise TypeError(f"text value must be str or None, got {type(value).__name__}") + text = unicodedata.normalize("NFC", value.replace("\r\n", "\n").replace("\r", "\n")) + return text.strip() + + +def truncate_content_tokens( + kind: SegmentKind, + tokens: Sequence[TokenT], + *, + max_content_tokens: int = 510, +) -> list[TokenT]: + """Apply the versioned segment policy before model special tokens are added. + + Current requests retain approximately 3:1 head/tail content, previous + answers retain 1:1, and history retains the most recent content. + """ + if max_content_tokens <= 0: + raise ValueError("max_content_tokens must be positive") + values = list(tokens) + if len(values) <= max_content_tokens: + return values + if kind == "recent_history": + return values[-max_content_tokens:] + if kind == "current_request": + head = (max_content_tokens * 3 + 3) // 4 + elif kind == "previous_answer": + head = (max_content_tokens + 1) // 2 + else: + raise ValueError(f"unsupported segment kind: {kind!r}") + tail = max_content_tokens - head + return values[:head] + (values[-tail:] if tail else []) + + +def sample_input(row: Mapping[str, object]) -> Mapping[str, object]: + value = row.get("input") + if value is None: + return row + if not isinstance(value, Mapping): + raise TypeError("row.input must be a mapping") + return value + + +def extract_text_segments(row: Mapping[str, object]) -> TextSegments: + payload = sample_input(row) + current = canonicalize_text(payload.get("current_request")) + if not current: + raise ValueError("input.current_request must be non-empty") + + raw_history = payload.get("history_user") or [] + if isinstance(raw_history, (str, bytes)) or not isinstance(raw_history, Sequence): + raise TypeError("input.history_user must be a sequence of strings") + turns = [canonicalize_text(value) for value in raw_history] + turns = [value for value in turns if value][-HISTORY_MAX_TURNS:] + history = HISTORY_SEPARATOR.join(turns) + previous = canonicalize_text(payload.get("previous_answer")) + return TextSegments(current, history, previous) diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/artifact_manifest.json b/src/opensquilla/squilla_router/lightgbm_model/assets/artifact_manifest.json new file mode 100644 index 0000000000..a9fc83e77d --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/artifact_manifest.json @@ -0,0 +1,110 @@ +{ + "schema_version": 1, + "files": [ + { + "path": "bundles/intent.json", + "size_bytes": 5331, + "sha256": "12f9706ba34fb681008d19a9fd0a9cdb871af47215a4cf7f3aa085557cc5bb2f" + }, + { + "path": "bundles/tier.json", + "size_bytes": 5308, + "sha256": "7a4d0dd5617559d912488866aea4c311ba6dfec64608bcfe866c48477e1e5650" + }, + { + "path": "intent/calibration.json", + "size_bytes": 116, + "sha256": "fd4e2361aa56276eb2d0ca5b3990e510dc77e255310fbb38a4f3f691140bd781" + }, + { + "path": "intent/golden_vectors.json", + "size_bytes": 44966, + "sha256": "958a84d20e6107e465df5427cc199d774ec95e44eb4bd6d4db13857b74645eca" + }, + { + "path": "intent/model.txt.gz", + "size_bytes": 7290131, + "sha256": "f82821f91e20a8536a1b7cb88c494b4659e26bcc9e1ef47b354a7728830bf482" + }, + { + "path": "model_manifest.json", + "size_bytes": 7010, + "sha256": "29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357" + }, + { + "path": "preprocess/feature/artifacts/feature_recipe.json", + "size_bytes": 2249, + "sha256": "c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d" + }, + { + "path": "preprocess/feature/artifacts/feature_registry.json", + "size_bytes": 17937, + "sha256": "e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17" + }, + { + "path": "preprocess/feature/artifacts/pca64.joblib", + "size_bytes": 135471, + "sha256": "86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4" + }, + { + "path": "preprocess/feature/artifacts/runtime_manifest.json", + "size_bytes": 2887, + "sha256": "2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c" + }, + { + "path": "preprocess/feature/artifacts/svd100.joblib", + "size_bytes": 4002047, + "sha256": "76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc" + }, + { + "path": "preprocess/feature/artifacts/tfidf_char_wb_2_4.joblib", + "size_bytes": 298451, + "sha256": "95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312" + }, + { + "path": "preprocess/feature_manifest.json", + "size_bytes": 4879, + "sha256": "a2b66a973653464652d9a7ffd0da25a384b5bf8fa0f11dbe8a3b0c3d1205d5eb" + }, + { + "path": "preprocess/preprocess_copy_manifest.json", + "size_bytes": 5140, + "sha256": "907a9b6f2ebdfad491fc98b3df13d4e8575077e581919e7c81f5a90847dde299" + }, + { + "path": "provenance.json", + "size_bytes": 3108, + "sha256": "5af7f31c2b226a005320f497ed452e581aa1485672a10cc04ca7e9f16ec64b17" + }, + { + "path": "runtime_compatibility.json", + "size_bytes": 11031, + "sha256": "3c026494f69653de63294fd40542cc79de422dcf43dadd108d372b34ba810d41" + }, + { + "path": "source_parity_cases.json", + "size_bytes": 87745, + "sha256": "781fc332e8de0c338ee99e90a7c37ae7ac37f489044ed986b07808bf636f26a0" + }, + { + "path": "tier/calibration.json", + "size_bytes": 113, + "sha256": "422c930f7293464fef377f7992f08040ac086c67b161a0e0b12a05458f759c51" + }, + { + "path": "tier/golden_vectors.json", + "size_bytes": 45147, + "sha256": "6d98be540e1d008882623ea3d4b210dc556824d6da1a8c6838451828fc843691" + }, + { + "path": "tier/model.txt.gz", + "size_bytes": 12238618, + "sha256": "60858e46ec2ddd090a5fa7a19cbb8011e71f944740f4b4033845c67764526b15" + }, + { + "path": "upstream-pyproject.toml", + "size_bytes": 1444, + "sha256": "55f36bb2fcd47dce832b40868054dfa6e72b527911c60d08840031da955fd845" + } + ] +} diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/intent.json b/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/intent.json new file mode 100644 index 0000000000..84c8ea253b --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/intent.json @@ -0,0 +1 @@ +{"dataset_manifest_hash":"sha256:a190a4a154d493631f1811bfaac7ce14b6d23c3104af743b6fd605bb6fc0d609","dataset_version":"router_training.2026-07-13.1+99d6ff75","feature_view_id":"sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c","input_schema_version":"lightgbm_380.v1","model_artifacts":[{"artifact_type":"temperature_calibration","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/calibration.json","sha256":"sha256:fd4e2361aa56276eb2d0ca5b3990e510dc77e255310fbb38a4f3f691140bd781","size":116,"uri":null},{"artifact_type":"golden_vectors","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/golden_vectors.json","sha256":"sha256:958a84d20e6107e465df5427cc199d774ec95e44eb4bd6d4db13857b74645eca","size":44966,"uri":null},{"artifact_type":"training_metrics","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/metrics.json","sha256":"sha256:8724fe41f1d1d8f00aa15f4f05160c511f356b6e616a1939afeaac3e8eaf4c59","size":3442,"uri":null},{"artifact_type":"lightgbm_model","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/model.txt","sha256":"sha256:fde9666455e2af232e7e6b00fed17d0384db92d0d064303585ad7cc68c14d9e2","size":16745339,"uri":null}],"model_type":"lightgbm","output_labels":["new_task","continue","redo"],"preprocess_artifacts":[{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null},{"artifact_type":"feature_registry","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null},{"artifact_type":"transformer_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null},{"artifact_type":"feature_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature_manifest.json","sha256":"sha256:a2b66a973653464652d9a7ffd0da25a384b5bf8fa0f11dbe8a3b0c3d1205d5eb","size":4879,"uri":null}],"preprocess_manifest":{"artifact_type":"preprocess_copy_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/preprocess_copy_manifest.json","sha256":"sha256:907a9b6f2ebdfad491fc98b3df13d4e8575077e581919e7c81f5a90847dde299","size":5140,"uri":null},"run_id":"lgbm-99d6ff75-d444135-a1-intent","run_manifest":{"artifact_type":"run_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/run_manifest.json","sha256":"sha256:ea3979d56951c88501f5ad2a609c68225b3e16d5b5eb181aff5365bbef6745c5","size":5692885,"uri":null},"runtime":{"engine":"lightgbm","feature_dimension":380,"feature_version":"lightgbm_380.v1"},"split_hash":"sha256:89eccc5d9b221c15004b146417a7226b162ac0dceef5bf69457302f835c8a917","target":"intent","training_code_digest":"git:d4441356584423be07361495e55b488c95c5db43"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/tier.json b/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/tier.json new file mode 100644 index 0000000000..f6df2e0984 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/bundles/tier.json @@ -0,0 +1 @@ +{"dataset_manifest_hash":"sha256:a190a4a154d493631f1811bfaac7ce14b6d23c3104af743b6fd605bb6fc0d609","dataset_version":"router_training.2026-07-13.1+99d6ff75","feature_view_id":"sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c","input_schema_version":"lightgbm_380.v1","model_artifacts":[{"artifact_type":"temperature_calibration","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/calibration.json","sha256":"sha256:422c930f7293464fef377f7992f08040ac086c67b161a0e0b12a05458f759c51","size":113,"uri":null},{"artifact_type":"golden_vectors","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/golden_vectors.json","sha256":"sha256:6d98be540e1d008882623ea3d4b210dc556824d6da1a8c6838451828fc843691","size":45147,"uri":null},{"artifact_type":"training_metrics","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/metrics.json","sha256":"sha256:4b7e7d724c21326b131ef307ea21bbe7f611e364054d598b27194e1f43cae482","size":3660,"uri":null},{"artifact_type":"lightgbm_model","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/model.txt","sha256":"sha256:0a49539b8f26ca40945cfad01f32f84d40cfd3fedeceffd2480a0779db5dc8d1","size":28019262,"uri":null}],"model_type":"lightgbm","output_labels":["C0","C1","C2","C3"],"preprocess_artifacts":[{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null},{"artifact_type":"feature_registry","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null},{"artifact_type":"transformer_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null},{"artifact_type":"feature_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature_manifest.json","sha256":"sha256:a2b66a973653464652d9a7ffd0da25a384b5bf8fa0f11dbe8a3b0c3d1205d5eb","size":4879,"uri":null}],"preprocess_manifest":{"artifact_type":"preprocess_copy_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/preprocess_copy_manifest.json","sha256":"sha256:907a9b6f2ebdfad491fc98b3df13d4e8575077e581919e7c81f5a90847dde299","size":5140,"uri":null},"run_id":"lgbm-99d6ff75-d444135-a1-tier","run_manifest":{"artifact_type":"run_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/run_manifest.json","sha256":"sha256:91740c5486dbd95725f009685b805f50c9c5d94fb2ab89152c913472a2b67d9a","size":5693087,"uri":null},"runtime":{"engine":"lightgbm","feature_dimension":380,"feature_version":"lightgbm_380.v1"},"split_hash":"sha256:89eccc5d9b221c15004b146417a7226b162ac0dceef5bf69457302f835c8a917","target":"tier","training_code_digest":"git:d4441356584423be07361495e55b488c95c5db43"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/intent/calibration.json b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/calibration.json new file mode 100644 index 0000000000..28bf5bf347 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/calibration.json @@ -0,0 +1 @@ +{"after_loss":0.10190265069370068,"before_loss":0.10245313023327918,"enabled":true,"temperature":1.0743778577600736} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/intent/golden_vectors.json b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/golden_vectors.json new file mode 100644 index 0000000000..66769c183c --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/golden_vectors.json @@ -0,0 +1 @@ +{"calibration":{"after_loss":0.10190265069370068,"before_loss":0.10245313023327918,"enabled":true,"temperature":1.0743778577600736},"cases":[{"expected":{"calibrated_class":"continue","calibrated_probabilities":[0.02096741046710511,0.9766511758531028,0.002381413679792216],"uncalibrated_class":"continue","uncalibrated_probabilities":[0.01585302491327976,0.9826154056666995,0.00153156942002107]},"features":[0.2271927297115326,0.001246139407157898,-0.10437075048685074,0.01772419363260269,0.01426348090171814,-3.739446401596069e-05,-0.03173252195119858,-0.08780141174793243,0.12102435529232025,-0.04758082330226898,-0.0014238283038139343,-0.06956596672534943,-0.043786417692899704,0.10375268757343292,0.05372927337884903,0.03535483032464981,-0.016197949647903442,-0.02530694752931595,0.023393090814352036,0.008109688758850098,0.05698921158909798,0.06294253468513489,0.04649005085229874,0.025195952504873276,-0.024891585111618042,0.04879217594861984,0.127475768327713,-0.040658071637153625,0.00022579357028007507,-0.0011517815291881561,-0.07425421476364136,-0.02780088782310486,-0.08883119374513626,-0.06902464479207993,-0.024615708738565445,-0.03350311517715454,0.021304909139871597,-0.05717994272708893,0.06704212725162506,-0.04403472691774368,-0.0399230495095253,-0.014289498329162598,0.03571464493870735,-0.011777466163039207,0.04950343444943428,0.022780271247029305,0.0007449798285961151,-0.0361713282763958,-0.009494569152593613,-0.014010053128004074,-0.04710948467254639,-0.014626255258917809,0.03404469043016434,-0.04297608882188797,0.00980566255748272,0.004397526383399963,0.028575431555509567,0.0405898317694664,0.01947510614991188,-0.024809394031763077,0.06188925355672836,-0.018722359091043472,-0.010530335828661919,-0.01185736432671547,0.5809037089347839,-0.23736044764518738,-0.10745149105787277,0.006178508047014475,-0.02191121317446232,0.007091960404068232,-0.04107978940010071,-0.011022726073861122,-0.031136581674218178,0.04225784167647362,-0.02634362317621708,0.008800599724054337,0.007005966734141111,0.013335313647985458,-0.07850735634565353,-0.013117359951138496,-0.008246433921158314,0.018294773995876312,0.019353579729795456,-0.012446426786482334,0.05200396105647087,-0.04504302516579628,-0.01444289367645979,-0.022888807579874992,0.027534203603863716,-0.0006576913874596357,0.022911500185728073,-0.02654639072716236,0.022586844861507416,-0.014555644243955612,-0.013579505495727062,0.11683805286884308,0.05537471920251846,-0.03189223259687424,-0.010020270012319088,-0.03381597250699997,-0.013695389032363892,-0.017883218824863434,0.030015062540769577,-0.02903672307729721,0.03856406733393669,0.07508080452680588,-0.013863369822502136,0.010825928300619125,-0.021285386756062508,0.0018973819678649306,0.03730952367186546,-0.011926699429750443,-0.04695291817188263,0.02320888824760914,0.014188681729137897,0.022535085678100586,0.01139330118894577,-0.013970562256872654,-0.047133151441812515,-0.009701546281576157,0.055175114423036575,0.006098239682614803,0.014667010866105556,-0.010524705983698368,-0.001014233916066587,0.02246156521141529,-0.05898670852184296,0.014115777797996998,0.028157755732536316,-0.013936103321611881,-0.037373993545770645,-0.00061549007659778,-0.02961242012679577,0.018494969233870506,0.015527116134762764,0.02074209600687027,0.08500168472528458,-0.04745620861649513,-0.0057913693599402905,0.00409818347543478,-0.011102968826889992,-0.06644615530967712,0.016051409766077995,0.0002879189560189843,-0.053008779883384705,0.016214828938245773,-0.005217639729380608,-0.040338627994060516,0.031159821897745132,0.020004453137516975,0.003747834824025631,-0.002765173790976405,0.0010477961041033268,0.007663321681320667,0.01311330683529377,0.014326324686408043,-0.020166615024209023,0.0027780558448284864,-0.003195313038304448,-0.030520016327500343,0.028931304812431335,0.02770138345658779,0.010546505451202393,-0.004132099915295839,667.0,107.0,9.0,74.11111450195312,0.0,0.7586206793785095,0.010494752787053585,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.8130841255187988,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.20690521597862244,0.07986576855182648,-0.13348063826560974,0.07952940464019775,0.13670378923416138,-0.06498891115188599,0.04904235154390335,-0.000582575798034668,0.015444375574588776,-0.04919424653053284,-0.003975216299295425,-0.038343414664268494,0.06148838251829147,0.03783619403839111,0.08819665759801865,0.13091467320919037,-0.010391121730208397,0.03408069163560867,0.02629983052611351,-0.03461834415793419,0.02897150255739689,0.015287630259990692,0.03880525380373001,0.06024354323744774,-0.05170189589262009,0.08152291178703308,0.046960532665252686,0.029616951942443848,0.012674437835812569,-0.0012077763676643372,-0.010901479050517082,-0.01688673347234726,-0.053351305425167084,-0.0004506576806306839,0.07768270373344421,0.009407341480255127,0.013062307611107826,-0.012906529009342194,0.07584255188703537,-0.0052324142307043076,0.0002085082232952118,-0.02466236799955368,-0.03067990019917488,-0.014758479781448841,0.0024167727679014206,-0.029470134526491165,-0.03971980884671211,0.04222283139824867,0.03772910684347153,-0.03085104562342167,-0.005788015201687813,0.006761245429515839,0.029275519773364067,0.018952321261167526,0.06055397540330887,-0.018416501581668854,-0.0597730427980423,0.011572964489459991,-0.009726237505674362,0.022801436483860016,0.05842212215065956,-0.02787916734814644,0.05861983448266983,0.022746212780475616,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25851574540138245,-0.005625709891319275,-0.14803990721702576,-0.011065270751714706,-0.040153101086616516,-0.02600717730820179,-0.10038043558597565,-0.12314744293689728,0.13950642943382263,-0.09533976018428802,-0.03284512460231781,-0.03396695852279663,-0.10570694506168365,0.07752451300621033,0.03476915508508682,-0.0004689851775765419,-0.019155379384756088,-0.028411738574504852,-0.00870051234960556,0.06731821596622467,0.05067578703165054,0.05769793689250946,0.010820083320140839,0.08109746873378754,-0.06373929232358932,0.007825173437595367,0.1170244812965393,-0.04806849732995033,-0.019257918000221252,-0.020753532648086548,0.036192089319229126,0.04225718602538109,-0.07102092355489731,0.029877817258238792,-0.06279623508453369,-0.08266288042068481,0.014803148806095123,-0.02166913077235222,0.059027452021837234,-0.019785135984420776,-0.005562368780374527,0.025854356586933136,0.04117029905319214,-0.005888170562684536,0.02993001788854599,-0.04815562441945076,0.004443892277777195,-0.01729757711291313,0.021647192537784576,-0.012897532433271408,0.0030643418431282043,0.006634123623371124,-0.002868982031941414,-0.03451944887638092,-0.025960253551602364,-0.02031160145998001,-0.04303538799285889,-0.0034492909908294678,0.022988591343164444,0.021399952471256256,0.03248748183250427,-0.007932446897029877,-0.007153713144361973,0.021771959960460663,1.0,0.0,0.0,0.0,0.0,1.0,0.6037870645523071,0.5010635256767273,0.0,0.3433283269405365,0.0,0.0,0.0,0.6037870645523071,0.0,0.0,0.650428831577301,0.5010635256767273,0.0,0.0,0.03099999949336052,0.0,0.0,0.0],"sample_id":"000252aa-f4e0-5e89-8d6f-aff9fd2d5206"},{"expected":{"calibrated_class":"continue","calibrated_probabilities":[0.1268107395225427,0.6484209633437408,0.22476829713371652],"uncalibrated_class":"continue","uncalibrated_probabilities":[0.11597288647033786,0.6695286124683021,0.21449850106136006]},"features":[-0.1874837875366211,0.2176925241947174,0.06428853422403336,0.13545964658260345,-0.1115870401263237,0.0099861528724432,0.07597401738166809,-0.10764665901660919,0.00798056647181511,0.03704189136624336,0.056351177394390106,-0.0047012511640787125,-0.0797542929649353,-0.08262322843074799,-0.021707363426685333,-0.006933874450623989,0.005838314071297646,-0.013192236423492432,-0.004748987033963203,-0.0344892218708992,0.026494061574339867,-0.025313949212431908,0.0021034181118011475,-0.07011718302965164,-0.028778642416000366,-0.12202045321464539,-0.03485279157757759,-0.05531344190239906,-0.015218351036310196,-0.06225254386663437,0.040932103991508484,0.033437829464673996,-0.041434600949287415,0.05650373920798302,0.10368061065673828,-0.015097599476575851,0.01957906410098076,0.0747511088848114,0.0473828949034214,-0.062329843640327454,0.06514695286750793,0.06173385679721832,0.006774061359465122,-0.050675518810749054,-0.01077330857515335,0.023026861250400543,0.058396853506565094,0.028509793803095818,-0.09342925250530243,-0.004606073256582022,0.010493021458387375,-0.07722865045070648,0.019380299374461174,0.11419067531824112,-0.037903208285570145,0.03052821010351181,0.070053830742836,-0.023908905684947968,-0.0064550600945949554,-0.02216017246246338,0.10233519971370697,0.055560484528541565,-0.00045209936797618866,-0.0002957955002784729,0.11455101519823074,-0.056232910603284836,0.22792470455169678,-0.006104484666138887,0.09314684569835663,0.04843873158097267,0.0013435783330351114,0.12666599452495575,0.06622447818517685,-0.6099086999893188,-0.36751922965049744,0.2606757879257202,-0.12699531018733978,0.006735207512974739,-0.04250539466738701,0.030610909685492516,-0.09311442077159882,0.043737150728702545,0.034807704389095306,-0.026976769790053368,-0.007944120094180107,-0.01189575158059597,0.006807413883507252,0.0034678520169109106,0.007284472230821848,0.008904725313186646,-0.07071011513471603,0.008374492637813091,0.0012507853098213673,-0.019026728346943855,-0.008588200435042381,0.007608428131788969,0.008587022311985493,-0.013321333564817905,0.041244905441999435,0.008774634450674057,0.00439375638961792,0.049406252801418304,-0.015705320984125137,-0.00575745664536953,-0.0011585857719182968,-0.014976742677390575,-0.018448997288942337,0.002091765170916915,0.020299576222896576,-0.02195868268609047,0.001667749136686325,-0.0231375340372324,0.03969189524650574,-0.02036229893565178,-0.03464166074991226,0.05138831213116646,0.02177385427057743,0.027486508712172508,-0.01051836647093296,-0.0368553102016449,-0.017400890588760376,-0.007928143255412579,0.013885268941521645,-0.00793690886348486,-0.035029541701078415,-0.007490832358598709,0.0004236078239046037,0.013714010827243328,-0.0048883091658353806,0.019454825669527054,0.012120245024561882,-0.0041168755851686,0.0006890093209221959,-0.013396501541137695,-0.002437783405184746,-0.0027153505943715572,0.009116011671721935,0.011589565314352512,0.004921626299619675,-0.013808726333081722,-0.023842064663767815,0.0011587608605623245,0.00026364639052189887,0.017284490168094635,0.024032341316342354,-0.02280246466398239,0.013259082101285458,0.018468793481588364,-0.0064735738560557365,-0.0026653907261788845,0.010859791189432144,-0.03332515433430672,0.0004055900499224663,-0.00474164355546236,-0.00794454850256443,-0.008148262277245522,0.00592180946841836,0.0025107853580266237,-0.009993497282266617,-0.013922176323831081,-0.004666192922741175,0.0002436682116240263,-0.003117332933470607,-0.007457343395799398,52.0,4.0,2.0,26.0,0.17307692766189575,0.3076923191547394,0.03846153989434242,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,-0.2862265110015869,0.17256686091423035,-0.1868399828672409,0.015174508094787598,-0.12454181909561157,0.00395769439637661,0.04224613308906555,-0.017343521118164062,0.03310355544090271,-0.08730609714984894,0.00030459463596343994,0.047994501888751984,0.07292042672634125,0.1024879515171051,-0.06193007528781891,-0.13482390344142914,0.028187241405248642,0.004325121641159058,-0.06313297152519226,-0.07993258535861969,-0.1246737688779831,0.03317397087812424,0.07209973782300949,0.014239337295293808,-0.11323697119951248,0.03723214566707611,0.047322891652584076,-0.0604277178645134,-0.06431597471237183,-0.02649221569299698,-0.006784040480852127,-0.04006977379322052,0.007135551422834396,-0.047497302293777466,0.010798942297697067,0.056955769658088684,0.028985608369112015,0.08208990097045898,-0.007202088832855225,-0.08791134506464005,-0.017683010548353195,0.011941760778427124,0.11721982806921005,-0.08398009091615677,-0.10318750143051147,0.008215878158807755,-0.03254402056336403,-0.03862113878130913,-0.021665411069989204,0.10764219611883163,0.10394182801246643,0.032839223742485046,0.03099760226905346,0.04276970028877258,-0.07122417539358139,-0.05207517743110657,0.041246529668569565,-0.05500771105289459,-0.08107751607894897,-0.08592216670513153,0.0350731760263443,-0.08083926141262054,0.00025872141122817993,0.033098720014095306,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.011472821235656738,0.13719293475151062,0.13046099245548248,0.03930112347006798,-0.07454492896795273,0.03285152465105057,0.0980183333158493,0.06790545582771301,-0.03534511849284172,0.08377839624881744,0.05611885339021683,-0.10800143331289291,0.10684721171855927,0.002453252673149109,0.010211490094661713,0.026865661144256592,-0.049917399883270264,-0.04614674672484398,-0.05911366641521454,0.012733085080981255,-0.07563598453998566,0.00832393579185009,-0.09579811990261078,-0.07032795250415802,0.03282683342695236,-0.05195179581642151,-0.005516473203897476,-0.052118100225925446,0.04483399540185928,-0.08768768608570099,0.04001373052597046,0.05201343819499016,0.02696644514799118,-0.01564711518585682,-0.026229102164506912,0.06770413368940353,-0.042394764721393585,-0.07484361529350281,-0.02739015221595764,0.1077592596411705,0.04224202409386635,0.05570201575756073,0.03548290953040123,0.004332289099693298,-0.059878453612327576,-0.08189834654331207,-0.010917811654508114,-0.045940425246953964,0.020323164761066437,0.01231573335826397,-0.016051407903432846,-0.15131132304668427,0.005112392827868462,0.06939328461885452,0.06258123368024826,-0.07234007120132446,-0.025966348126530647,-0.022198230028152466,-0.009217385202646255,0.009597547352313995,-0.04504867270588875,0.023675402626395226,0.05655419081449509,-0.014762938022613525,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.05000000074505806,0.0,0.0,0.0,0.0,0.0,0.0,0.39702919125556946,0.0,0.0,0.0,0.004050000105053186,0.0,0.0,0.0],"sample_id":"0002f865-997e-580a-9a6c-c93ff25b9e3c"},{"expected":{"calibrated_class":"new_task","calibrated_probabilities":[0.9970275615420362,0.0028056139558645487,0.00016682450209917925],"uncalibrated_class":"new_task","uncalibrated_probabilities":[0.9980979379144014,0.0018145955813410994,8.746650425756902e-05]},"features":[0.19540908932685852,-0.047253310680389404,-0.14079995453357697,-0.015178505331277847,0.10812124609947205,-0.06642849743366241,0.061161208897829056,-0.07694824039936066,-0.10192044079303741,-0.04116915911436081,-0.07972553372383118,-0.021203873679041862,-0.04534922167658806,0.08767305314540863,-0.02003081887960434,-0.011741810478270054,0.02953232079744339,0.08988482505083084,-0.0543738454580307,-0.0705256313085556,0.052022602409124374,0.0020773839205503464,0.00431244820356369,0.04831286519765854,0.0037041008472442627,-0.014593157917261124,0.0702851265668869,0.06293322145938873,0.02189391292631626,0.09603595733642578,0.00812646932899952,0.04647457227110863,0.07754935324192047,0.021456608548760414,0.018039830029010773,-0.017397407442331314,0.04396618902683258,-0.00898277759552002,0.0002991780638694763,0.0021367091685533524,0.008052382618188858,-0.009035177528858185,0.021388068795204163,-0.009386769495904446,-0.028928712010383606,-0.023818738758563995,0.04139406234025955,-0.017774265259504318,-0.0313352607190609,-0.032247450202703476,0.04742889478802681,0.027063466608524323,0.03636494278907776,-0.04081060737371445,-0.013807671144604683,-0.046351876109838486,0.01619913801550865,0.010842926800251007,-0.009679201990365982,0.003761284053325653,-0.023194488137960434,-0.0148774404078722,-0.05038382112979889,0.02326323837041855,0.6614606976509094,-0.22057583928108215,-0.03647168353199959,0.004030977375805378,0.11546221375465393,0.008453442715108395,-0.1589743196964264,0.013450387865304947,-0.01729252003133297,-0.02076835185289383,-0.024298878386616707,0.022518722340464592,-0.06554815173149109,0.043599728494882584,-0.12890391051769257,-0.09101846069097519,0.06824609637260437,-0.05367758125066757,-0.02207203209400177,0.05135158449411392,-0.031593918800354004,0.06477200239896774,-0.020217837765812874,-0.0036277163308113813,0.0165749192237854,0.00747483829036355,0.033838748931884766,0.07083086669445038,0.11590000987052917,0.01032948773354292,-0.007377207279205322,-0.00995755847543478,-0.10399526357650757,0.10286544263362885,-0.05456622689962387,-0.052865851670503616,-0.05242273211479187,0.04603726416826248,0.07715699076652527,-0.043604012578725815,-0.01443322654813528,-0.014743600971996784,0.010603356175124645,-0.0539909228682518,-0.061636969447135925,0.03704022616147995,-0.01924794167280197,0.06281458586454391,0.04747948423027992,-0.010786580853164196,0.06078368052840233,-0.10957151651382446,-0.011901341378688812,0.038693591952323914,0.11534373462200165,0.07508765161037445,0.09067130833864212,0.016845304518938065,-0.02132858708500862,0.014471281319856644,-0.05352618172764778,-0.03216048702597618,-0.08358399569988251,-0.01018597837537527,-0.0002448071027174592,-0.021786876022815704,-0.0021815351210534573,-0.02533668838441372,-0.04969622567296028,-0.009036943316459656,0.06588813662528992,0.015036833472549915,-0.01879161410033703,0.008270316757261753,-0.02195897325873375,0.005621667020022869,-0.0577344074845314,0.012832784093916416,0.010645818896591663,-0.0019234968349337578,-0.043603502213954926,0.017701048403978348,-0.007054146844893694,-0.054289381951093674,0.03335646912455559,0.02476549707353115,-0.019768649712204933,0.011911401525139809,-0.01632746309041977,0.05129676312208176,0.07840583473443985,0.011382411234080791,0.02098800241947174,0.008239767514169216,0.05375731736421585,-0.00249406136572361,0.00954885222017765,-0.02480972185730934,-0.00407508434727788,-0.05382638797163963,1787.0,247.0,39.0,45.82051467895508,0.0,0.7672076225280762,0.007274762261658907,0.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,10.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.023503078147768974,0.7206477522850037,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7488852739334106,0.0,0.0,0.0,0.022299999371170998,0.0,0.0,0.0],"sample_id":"000699fa-5cbb-5f4d-9e12-5992c1c9b25c"},{"expected":{"calibrated_class":"new_task","calibrated_probabilities":[0.9895418179954961,0.01029305973517536,0.00016512226932851025],"uncalibrated_class":"new_task","uncalibrated_probabilities":[0.9925616239274656,0.007351648525991328,8.672754654309448e-05]},"features":[0.11209720373153687,-0.12402911484241486,0.05929974839091301,-0.08892060816287994,-0.06548567116260529,0.1282137781381607,-0.06728805601596832,-0.0743999183177948,0.13771075010299683,0.025610849261283875,-0.05165467411279678,0.14387625455856323,0.04347740858793259,0.12157003581523895,0.03183617442846298,-0.0010119713842868805,0.032527051866054535,0.07443410903215408,-0.028669660910964012,-0.05103444680571556,0.03457760810852051,-0.08999118208885193,-0.02902219258248806,-0.07157240808010101,0.01598650962114334,-0.038567930459976196,0.00039770081639289856,-0.02083064243197441,0.03307506442070007,-0.05195268243551254,-0.045299045741558075,-0.07790645956993103,0.008810382336378098,-0.0851532518863678,-0.05523855239152908,-0.09891001880168915,0.07712686061859131,-0.04922657087445259,-0.087241530418396,0.02410402148962021,0.02384546771645546,0.0019202083349227905,0.025659849867224693,0.025600941851735115,0.01668241247534752,0.0017912425100803375,0.006999087519943714,0.03375096991658211,0.010273896157741547,0.03339289501309395,-0.016898291185498238,0.04855448380112648,-0.06171003729104996,0.020207280293107033,0.008497240021824837,0.005655646324157715,0.012172531336545944,-0.02699529379606247,-0.014359917491674423,0.03803511708974838,0.004665907472372055,0.01839682087302208,0.02081739343702793,0.04687214642763138,0.3616150915622711,-0.06518188863992691,-0.06088462471961975,0.008472451940178871,-0.021803531795740128,0.17779166996479034,-0.03320243954658508,-0.2866930663585663,1.3287528418004513e-05,-0.04797571897506714,0.016194572672247887,-0.011384721845388412,0.0029244162142276764,-0.03415309265255928,0.005168582312762737,-0.06493571400642395,-0.047311823815107346,-0.014187927357852459,0.0393703319132328,0.06974450498819351,0.07178333401679993,-0.023675573989748955,0.05400872975587845,0.028546515852212906,-0.040372420102357864,-0.01320929266512394,0.05163532868027687,0.06547750532627106,0.007037586998194456,-0.006095025688409805,-0.1221480667591095,0.03208451718091965,-0.006700484547764063,0.008978881873190403,-0.030016686767339706,-0.03353698551654816,-0.06825315952301025,-0.04786914214491844,-0.05254591256380081,0.008953824639320374,-0.08332259953022003,-0.07674261182546616,0.032826684415340424,0.0639144629240036,-0.005530219059437513,0.015915237367153168,-0.04671655595302582,0.015587035566568375,-0.008853457868099213,-0.014578169211745262,-0.03641270846128464,0.0025888755917549133,-0.012147142551839352,-0.033751364797353745,0.003661959897726774,-0.07297053933143616,0.036091189831495285,-0.09823641926050186,-0.07962331920862198,0.010563994757831097,-0.0041709174402058125,-0.004610935226082802,0.02334919385612011,-0.05922475829720497,-0.030164945870637894,-0.0476500429213047,0.07510323822498322,-0.018848005682229996,0.06003449484705925,-0.06430740654468536,-0.02290743589401245,0.03247528150677681,0.02638411521911621,0.002465088851749897,0.007417704910039902,0.012197320349514484,0.01501381117850542,-0.03725264221429825,-0.016987212002277374,-0.03282485902309418,-0.03783441334962845,-0.007836787961423397,-0.010010471567511559,-0.0007446326198987663,-0.0044931527227163315,-0.005748489871621132,-0.01149279996752739,0.006743249483406544,0.0335051566362381,-0.015794767066836357,-0.004014546517282724,-0.04179058223962784,0.011014490388333797,0.04197033494710922,0.026642365381121635,-0.00937097892165184,0.002681109355762601,0.007876682095229626,-3.8350000977516174e-05,-0.015294216573238373,217.0,33.0,4.0,54.25,0.04608295112848282,0.7327188849449158,0.0138248847797513,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9090909361839294,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5384495258331299,0.0,0.0,0.0,0.0027000000700354576,0.0,0.0,0.0],"sample_id":"00087820-69a2-5d81-bc6e-c241f0fe4df0"},{"expected":{"calibrated_class":"new_task","calibrated_probabilities":[0.9893893098809851,0.010354678782640016,0.0002560113363750448],"uncalibrated_class":"new_task","uncalibrated_probabilities":[0.9924616446052235,0.007399422749465854,0.00013893264531083822]},"features":[-0.21443097293376923,-0.03253932297229767,0.006414398550987244,-0.12273433804512024,0.10528185963630676,0.038162559270858765,0.04085492715239525,0.04400382190942764,-0.06923475861549377,-0.1514551192522049,-0.04269780218601227,-0.10690807551145554,0.025273840874433517,-0.05786771699786186,0.02425777167081833,-0.21089959144592285,0.07756182551383972,0.038319431245326996,0.02584823966026306,0.024759288877248764,-0.022201795130968094,0.1325124055147171,-0.09122727811336517,-0.06828919798135757,-0.04743994027376175,-0.04582204669713974,-0.06645247340202332,-0.054839838296175,0.03437410295009613,0.0194135420024395,0.05220896750688553,-0.014453854411840439,-0.08763673156499863,-0.017129046842455864,-0.026574529707431793,-0.016001813113689423,0.024945814162492752,-0.07823220640420914,-0.05607149004936218,0.008936978876590729,0.014976035803556442,0.06495550274848938,-0.024200353771448135,0.028098778799176216,0.0149384755641222,0.06248562037944794,-0.0602782666683197,0.037504106760025024,0.027691885828971863,-0.08455681800842285,0.028594449162483215,-0.08716168999671936,-0.002255653962492943,0.03026914969086647,-0.08939752727746964,-0.026786919683218002,0.08703835308551788,-0.07891508936882019,-0.032081957906484604,-0.02035539597272873,0.021478280425071716,-0.05491426959633827,-0.016687948256731033,-0.045607633888721466,0.1879265159368515,-0.03630731627345085,-0.05260998755693436,0.01222106721252203,0.06113718822598457,0.032737743109464645,-0.0790974423289299,0.017041653394699097,-0.028669418767094612,0.027299392968416214,0.0366990864276886,0.051659781485795975,0.005016784183681011,0.028627794235944748,0.013902704231441021,-0.034271493554115295,0.004073935095220804,0.029203055426478386,0.036563508212566376,-0.0017863740213215351,0.004935266450047493,-0.016652371734380722,0.011673995293676853,0.03721578046679497,-0.028436310589313507,-0.05414505675435066,0.02604161947965622,0.04848705232143402,-0.026995178312063217,0.016735687851905823,-0.004498605616390705,0.023939257487654686,0.043548740446567535,0.05413489043712616,-0.010718585923314095,-0.00599986407905817,-0.05247047170996666,0.08531621098518372,0.008524174802005291,-0.01465198677033186,-0.01241111196577549,-0.032699886709451675,-0.037856657058000565,-0.01700231246650219,-0.016780773177742958,0.025603175163269043,-0.004906359128654003,0.046014729887247086,-0.02585734613239765,0.01671752892434597,0.010761376470327377,0.03991401568055153,-0.030733615159988403,-0.02459164708852768,0.006458860822021961,-0.019810743629932404,-0.021189115941524506,-0.07587561011314392,-0.0842178612947464,-0.004108154680579901,-0.029645761474967003,-0.01651395671069622,0.05348358675837517,-0.014553159475326538,0.005192369222640991,-0.004440378863364458,0.028301924467086792,-0.046893659979104996,0.01995844393968582,0.00554522592574358,0.0008118932601064444,-0.029559362679719925,0.07060229033231735,0.09091116487979889,-0.05000835657119751,-0.03746037930250168,-0.002559804590418935,0.03548024222254753,0.0078086890280246735,0.05882638692855835,-0.043559618294239044,0.02131158858537674,0.007510500028729439,-0.032617080956697464,-0.06425207108259201,0.03223419934511185,-0.014143096283078194,0.02276645228266716,0.03902644291520119,-0.0926167368888855,-0.05864541232585907,0.06842365115880966,-0.056694477796554565,0.005215019918978214,-0.05732349678874016,-0.07090888917446136,0.00497719831764698,0.010720727033913136,0.05744805559515953,-0.01973043382167816,139.0,10.0,1.0,139.0,0.2517985701560974,0.5899280309677124,0.014388489536941051,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.4892086386680603,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.14388489723205566,0.49416422843933105,0.0,0.0,0.0,0.0017000000225380063,0.0,0.0,0.0],"sample_id":"0009f8ab-24a8-5d24-bcf3-1f4c38c980ba"},{"expected":{"calibrated_class":"continue","calibrated_probabilities":[0.0001837339728052543,0.9975423343046794,0.0022739317225152697],"uncalibrated_class":"continue","uncalibrated_probabilities":[9.70073599122914e-05,0.9984553645842424,0.0014476280558452983]},"features":[0.22351911664009094,-0.06489183008670807,0.11539721488952637,0.027557887136936188,0.04313138127326965,0.03544566035270691,-0.09007439017295837,0.04602908715605736,-0.023381242528557777,-0.06035175174474716,0.09471236169338226,-0.016682425513863564,-0.06948487460613251,-0.018239736557006836,0.06351057440042496,-0.0070722950622439384,-0.07994377613067627,0.0848364606499672,0.05249757692217827,-0.02921859547495842,-0.03882705792784691,-0.00973636656999588,-0.0987662822008133,-0.012674175202846527,-0.0366230234503746,0.016497492790222168,-0.06913010776042938,0.012026011943817139,0.0070295874029397964,-0.026451513171195984,-0.019596464931964874,-0.014785382896661758,0.017339857295155525,-0.015383703634142876,-0.0542544387280941,-0.057140108197927475,-0.048959508538246155,-0.027112003415822983,0.04252529516816139,-0.02076786756515503,0.013263050466775894,0.04666602239012718,0.01077244896441698,0.02583235315978527,0.00291619636118412,-0.016126390546560287,-0.00522217433899641,0.02768523246049881,-0.028627652674913406,0.028060002252459526,0.050150807946920395,0.03644201159477234,0.016796989366412163,0.03765019774436951,0.05639893561601639,-0.0739612951874733,0.022418197244405746,-0.0066039785742759705,-0.02451123669743538,0.01530570536851883,0.04869310185313225,-0.02777782455086708,0.04067277908325195,-0.004140473902225494,0.5369227528572083,-0.2043306976556778,0.05510263890028,-0.010604418814182281,-0.08766032010316849,-0.0209273062646389,0.05440448597073555,0.02112908475100994,-0.005515711847692728,0.02381301485002041,0.023236483335494995,-0.02708660252392292,0.026279041543602943,-0.00765078142285347,-0.0023283527698367834,0.07339730113744736,0.03427417203783989,-0.11621928960084915,0.002949499525129795,0.011231708340346813,-0.04588073119521141,-0.10669303685426712,-0.0944429412484169,0.002561239991337061,-0.03867844492197037,-0.014911435544490814,-0.055740877985954285,0.08283493667840958,-0.049476444721221924,-0.0039666155353188515,-0.054212577641010284,-0.06981246918439865,-0.055086880922317505,-0.05437793955206871,0.042533356696367264,-0.04428671672940254,0.05131808668375015,-0.04855937510728836,0.026860281825065613,-0.01180857140570879,-0.004623992834240198,0.020039185881614685,-0.02424144558608532,-0.029216058552265167,0.10458981245756149,-0.05033382028341293,-0.07451067119836807,-0.050739362835884094,0.0006428512278944254,0.1034996509552002,-0.05432857945561409,0.0789397656917572,-0.01547321304678917,-0.031000100076198578,0.018115133047103882,0.06510532647371292,-0.039998073130846024,0.022172836586833,0.0460551455616951,-0.020082714036107063,0.10459522157907486,-0.05206078290939331,-0.18109050393104553,0.040407150983810425,0.03595583140850067,-0.005941063165664673,0.05091599375009537,-0.03521044924855232,0.0036067813634872437,0.020734261721372604,0.038950879126787186,0.0008171928348019719,-0.07326509058475494,0.013924629427492619,-0.04031689092516899,0.018237126991152763,0.16919240355491638,-0.08194263279438019,-0.11573392897844315,0.004428189713507891,-0.10300866514444351,0.07136958092451096,0.08800923824310303,0.08664383739233017,0.057565052062273026,-0.054412778466939926,0.02079814299941063,-0.12150812894105911,-0.04150395840406418,0.10753680765628815,-0.004181038588285446,0.004343877080827951,0.02547955885529518,-0.044421564787626266,-0.08879668265581131,-0.039846353232860565,-0.04018222540616989,0.022254077717661858,-0.01272085215896368,-0.020157787948846817,332.0,50.0,2.0,166.0,0.0,0.8162650465965271,0.0060240961611270905,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.8799999952316284,1.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.23499754071235657,-0.26318681240081787,0.06405917555093765,-0.02991544082760811,0.053602904081344604,-0.13683877885341644,0.05032418668270111,0.0003335140645503998,0.019129730761051178,-0.02987843006849289,0.09102048724889755,0.05428265035152435,-0.04256462678313255,-0.1193566620349884,0.016283802688121796,0.014959382824599743,-0.020492933690547943,0.023225821554660797,-0.060250379145145416,-0.030512064695358276,0.00031853094696998596,-0.04599719122052193,0.0362083837389946,0.023527927696704865,0.006405867636203766,0.03571522980928421,0.013322710990905762,-0.019172348082065582,0.003703424707055092,0.018428128212690353,-0.027448154985904694,-0.009005293250083923,-0.0072441622614860535,0.021761344745755196,0.01440475508570671,-0.017553139477968216,-0.027310684323310852,-0.01739785261452198,-0.015169583261013031,-0.0036715976893901825,0.018193449825048447,-0.00810687243938446,-0.025057684630155563,-0.03965328261256218,0.03715769574046135,0.0328502431511879,-0.001557186245918274,0.029864078387618065,-0.08546066284179688,0.03523118048906326,-0.014618568122386932,0.027253098785877228,0.013523651286959648,-0.017163408920168877,-0.0018802210688591003,-0.024671953171491623,-0.02486938238143921,0.005807153880596161,-0.008122909814119339,0.02142464369535446,-0.005697567015886307,0.00589411286637187,0.01486135832965374,0.014298900961875916,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.054575115442276,0.14493490755558014,0.22252441942691803,0.0366363525390625,0.013203814625740051,-0.07705295830965042,0.10974062234163284,0.10018117725849152,-0.12079009413719177,0.07671321928501129,0.05269663780927658,-0.007306484505534172,0.0389615036547184,0.0075301602482795715,0.0629206970334053,-0.02704128623008728,0.008099481463432312,-0.018936730921268463,-0.08752991259098053,0.05290912464261055,0.1174381673336029,-0.08500537276268005,0.026767216622829437,-0.02194669097661972,-0.06184721738100052,-0.04749135300517082,-0.026500944048166275,-0.023106452077627182,0.05362864211201668,-0.07452715933322906,0.046276599168777466,-0.05881499499082565,0.07383692264556885,0.028420692309737206,-0.029844779521226883,0.06360189616680145,-0.02865973860025406,0.025582347065210342,0.0025666728615760803,0.01198771595954895,0.04269549623131752,-0.046107806265354156,-0.01083537470549345,-0.017131801694631577,-0.01725001260638237,-0.07334811985492706,0.012434176169335842,-0.027707185596227646,-0.00702204555273056,0.029455844312906265,-0.01539677008986473,-0.05655037984251976,0.027634521946310997,0.04302321374416351,0.008300958201289177,0.05402229726314545,-0.03803793340921402,8.655339479446411e-05,0.015047378838062286,0.04675915092229843,-0.019793514162302017,-0.006024087313562632,-0.0058452058583498,0.036484040319919586,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.022891566157341003,0.0,0.0,0.0,0.0,0.0,0.0,0.5808142423629761,0.0,0.0,0.0,0.07275000214576721,0.0,0.0,0.0],"sample_id":"000e6e41-2020-58c4-b480-eac14232b432"},{"expected":{"calibrated_class":"new_task","calibrated_probabilities":[0.9937033183775995,0.0059427191064418145,0.0003539625159588142],"uncalibrated_class":"new_task","uncalibrated_probabilities":[0.9957342913327704,0.004069206714840327,0.00019650195238930286]},"features":[0.21808144450187683,-0.2721843719482422,0.02939518541097641,0.015401434153318405,-0.008072532713413239,0.08890365809202194,0.0645640566945076,-0.04243731498718262,0.07589137554168701,-0.03474161773920059,-0.0013090521097183228,0.01292361132800579,-0.014788445085287094,-0.061618708074092865,0.025500096380710602,0.014088903553783894,-0.07558949291706085,0.040443189442157745,0.028856683522462845,0.015576638281345367,0.020391665399074554,-0.054976917803287506,-0.003909103572368622,-0.03708405792713165,-0.030966974794864655,0.0157085582613945,-0.05835799500346184,-0.03611917421221733,0.013729259371757507,0.0243782177567482,-0.012306781485676765,-0.035145021975040436,0.005054604262113571,0.02189350686967373,-0.044951435178518295,0.00894663855433464,0.0142088383436203,0.041174691170454025,-0.02426759898662567,0.0010841898620128632,-0.002197224646806717,0.014001019299030304,0.012225937098264694,-0.013020024634897709,-0.05309818685054779,0.02065250650048256,0.021127503365278244,0.013461899012327194,-0.033107489347457886,-0.02498629502952099,0.015742775052785873,-0.0353286974132061,-0.025463851168751717,-0.0399814173579216,-0.013768773525953293,0.014501526951789856,0.009662721306085587,0.016641758382320404,0.002970818430185318,-0.007721163332462311,0.005595531314611435,-0.013466274365782738,-0.005216076970100403,0.010352537035942078,0.8258389830589294,-0.0468934029340744,-0.0956348106265068,-0.0029014761094003916,-0.10546477884054184,-0.009773705154657364,0.0026977122761309147,-0.01757740043103695,-0.01421690359711647,0.05314561352133751,-0.03432178124785423,-0.03864560276269913,-0.05099838599562645,0.05583870783448219,-0.035584885627031326,-0.005010142922401428,0.05012229084968567,-0.03266821801662445,0.01199042797088623,-0.05361505597829819,0.013965629041194916,-0.123587466776371,0.059596870094537735,-0.03798217326402664,0.035810038447380066,0.01772828958928585,-0.12787382304668427,0.005613275337964296,-0.043277494609355927,0.011379877105355263,0.014897175133228302,-0.010976539924740791,0.04007665812969208,-0.006996288895606995,-0.046992991119623184,-0.011574090458452702,-0.023952845484018326,-0.013609279878437519,-0.0007270291680470109,0.0020077989902347326,0.005408259574323893,-0.028687849640846252,0.005759724415838718,0.061790261417627335,0.039359692484140396,-0.01967764087021351,-0.0210240688174963,-0.015653343871235847,-0.0028742430731654167,-0.004767056088894606,-0.025472192093729973,-0.007554817479103804,0.002336355857551098,0.009325929917395115,-0.0014350248966366053,0.005732553079724312,0.03547586500644684,-0.008118067868053913,-0.01791366934776306,0.005168799310922623,0.0073282853700220585,0.045443687587976456,0.015647681429982185,-0.03560911491513252,0.007982132025063038,0.010282964445650578,-0.029294010251760483,-0.00857546366751194,0.017619630321860313,-0.017314855009317398,-0.007117739412933588,-0.010918209329247475,0.0001579807576490566,-0.007039918098598719,0.017875153571367264,-0.009437991306185722,0.03918181359767914,0.013192237354815006,-0.00299853109754622,0.07471917569637299,0.004945221357047558,-0.011531668715178967,0.0006934214034117758,-0.012892676517367363,-0.03469490632414818,0.005725327879190445,0.02412128448486328,0.009849675931036472,0.043862082064151764,-0.004912249743938446,-0.009472689591348171,-0.009888171218335629,0.01447307039052248,0.026254814118146896,0.01488555409014225,0.011308151297271252,0.017560111358761787,-0.015634685754776,0.0015723493415862322,0.02839643694460392,3142.0,444.0,47.0,66.85106658935547,0.01336728222668171,0.7536600828170776,0.018141310662031174,0.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,9.0,0.0,1.0,1.0,0.0,1.0,0.0,2.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.24188415706157684,0.6441441178321838,1.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.8052933216094971,0.0,0.0,0.0,0.039250001311302185,0.0,0.0,0.0],"sample_id":"00157511-ecff-5062-a586-d1fbed107814"},{"expected":{"calibrated_class":"new_task","calibrated_probabilities":[0.9769978463285328,0.022586059419992466,0.000416094251474664],"uncalibrated_class":"new_task","uncalibrated_probabilities":[0.9826002976838405,0.01716475558062449,0.0002349467355349795]},"features":[0.24310734868049622,-0.04663367569446564,-0.1858833134174347,-0.10586197674274445,0.04934287071228027,0.018895858898758888,0.03583654761314392,-0.20439012348651886,-0.0030672959983348846,-0.07119214534759521,-0.02947312593460083,0.012727614492177963,0.01672106236219406,0.02849254012107849,0.025550231337547302,-0.11705746501684189,-0.037166811525821686,0.02829887717962265,-0.028604554012417793,0.0231003500521183,0.0011629974469542503,-0.011856642551720142,-0.018639661371707916,0.0409594401717186,0.060693465173244476,-0.0415867455303669,-0.04097294434905052,-0.016916051506996155,-0.03045235201716423,0.0746525451540947,0.0023551788181066513,0.012807954102754593,0.019307276234030724,0.013549303635954857,-0.06632891297340393,-0.028585368767380714,0.03433746472001076,-0.08225928992033005,-0.06106603145599365,-0.041030045598745346,-0.052577003836631775,0.008030004799365997,-0.0455164834856987,0.020005280151963234,0.04211346060037613,0.01210465282201767,-0.060914043337106705,0.02869914472103119,0.043962135910987854,0.03934496268630028,-0.02352392114698887,-0.05185718089342117,0.04045245051383972,-0.008130257949233055,0.012940118089318275,-0.012415260076522827,0.007803451269865036,-0.020089518278837204,0.024246206507086754,-0.059939853847026825,-0.023223262280225754,0.01258285716176033,0.022272994741797447,-0.010492794215679169,0.5942202806472778,-0.16481992602348328,-0.07688772678375244,0.009827574715018272,0.1471148580312729,-0.001346866018138826,-0.10546639561653137,-0.005903811659663916,-0.05782558023929596,0.07193639874458313,0.010647517628967762,0.014144006185233593,-0.02386944741010666,0.06380999833345413,-0.10531231015920639,-0.007530965842306614,0.10519272834062576,0.043572794646024704,0.05473439395427704,-0.13515613973140717,0.1623646318912506,0.11528650671243668,0.06023078039288521,-0.0115051856264472,0.034074749797582626,0.004879140295088291,0.09197424352169037,0.07500423491001129,-0.013631564565002918,0.014041346497833729,-0.08167006820440292,-0.022909726947546005,-0.00559030007570982,-0.0009610811830498278,0.12929369509220123,0.11379904299974442,-0.09578534215688705,0.024052955210208893,0.022499313578009605,0.007511920295655727,0.033178526908159256,0.026634864509105682,-0.051389385014772415,0.03518655523657799,4.211245686747134e-05,0.02040061354637146,0.010915733873844147,0.005513591226190329,-0.014247178100049496,0.021732430905103683,-0.0339391753077507,0.008241997100412846,-0.011603694409132004,-0.00908383447676897,0.026336440816521645,0.020180195569992065,0.04726851359009743,-0.06210743263363838,-0.06545297801494598,0.011805534362792969,0.039806731045246124,-0.031753845512866974,0.006314877886325121,-0.017914259806275368,-0.002328308764845133,0.06731835752725601,-0.01585571840405464,-0.004164720885455608,-0.0083444993942976,0.03270021080970764,0.08004375547170639,-0.006695848889648914,-0.034686412662267685,0.018947791308164597,-0.020329231396317482,0.004126678220927715,-0.007034870330244303,-0.01331740990281105,0.021403266116976738,0.002808097517117858,0.028774604201316833,0.009428524412214756,-0.008709914050996304,-0.039631735533475876,0.009671402163803577,-0.004530327394604683,0.009634044952690601,-0.04645044356584549,0.019901826977729797,-0.02343001775443554,-0.04850699380040169,-0.005046341102570295,-0.029165495187044144,0.030565043911337852,-0.010608343407511711,-0.027341807261109352,0.009501760825514793,0.012458055280148983,-0.03495737537741661,-0.009416071698069572,1417.0,193.0,29.0,48.86206817626953,0.0,0.7685250639915466,0.03669724613428116,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,12.0,9.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.019054340198636055,0.7305699586868286,0.0,2.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7257002592086792,0.0,0.0,0.0,0.01769999973475933,0.0,0.0,0.0],"sample_id":"00171d50-19ae-5f83-974f-a41d5108066a"}],"classes":["new_task","continue","redo"],"input_contract":{"features":"float32[batch,380]"},"output_contract":{"class_match":"exact","probability_atol":1e-06},"schema_version":"lightgbm_golden_vectors.v1","target":"intent","validation_policy_version":"lightgbm_golden_validation.v1"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/intent/model.txt.gz b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/model.txt.gz new file mode 100644 index 0000000000..ca7f0a7fd0 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/intent/model.txt.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f82821f91e20a8536a1b7cb88c494b4659e26bcc9e1ef47b354a7728830bf482 +size 7290131 diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/model_manifest.json b/src/opensquilla/squilla_router/lightgbm_model/assets/model_manifest.json new file mode 100644 index 0000000000..28057b95cf --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/model_manifest.json @@ -0,0 +1 @@ +{"created_at":"2026-09-03T17:45:34.990194Z","dataset_manifest_hash":"sha256:a190a4a154d493631f1811bfaac7ce14b6d23c3104af743b6fd605bb6fc0d609","dataset_version":"router_training.2026-07-13.1+99d6ff75","feature_view_id":"sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c","input_schema_version":"lightgbm_380.v1","model_set_id":"router-lightgbm-99d6ff75-d444135-a1","model_type":"lightgbm","models":[{"artifact_type":"classifier_bundle","path":"models/router/router-lightgbm-99d6ff75-d444135-a1/bundles/intent.json","sha256":"sha256:12f9706ba34fb681008d19a9fd0a9cdb871af47215a4cf7f3aa085557cc5bb2f","size":5331,"uri":null},{"artifact_type":"classifier_bundle","path":"models/router/router-lightgbm-99d6ff75-d444135-a1/bundles/tier.json","sha256":"sha256:7a4d0dd5617559d912488866aea4c311ba6dfec64608bcfe866c48477e1e5650","size":5308,"uri":null},{"artifact_type":"run_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/run_manifest.json","sha256":"sha256:ea3979d56951c88501f5ad2a609c68225b3e16d5b5eb181aff5365bbef6745c5","size":5692885,"uri":null},{"artifact_type":"temperature_calibration","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/calibration.json","sha256":"sha256:fd4e2361aa56276eb2d0ca5b3990e510dc77e255310fbb38a4f3f691140bd781","size":116,"uri":null},{"artifact_type":"golden_vectors","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/golden_vectors.json","sha256":"sha256:958a84d20e6107e465df5427cc199d774ec95e44eb4bd6d4db13857b74645eca","size":44966,"uri":null},{"artifact_type":"training_metrics","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/metrics.json","sha256":"sha256:8724fe41f1d1d8f00aa15f4f05160c511f356b6e616a1939afeaac3e8eaf4c59","size":3442,"uri":null},{"artifact_type":"lightgbm_model","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/intent/model.txt","sha256":"sha256:fde9666455e2af232e7e6b00fed17d0384db92d0d064303585ad7cc68c14d9e2","size":16745339,"uri":null},{"artifact_type":"run_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/run_manifest.json","sha256":"sha256:91740c5486dbd95725f009685b805f50c9c5d94fb2ab89152c913472a2b67d9a","size":5693087,"uri":null},{"artifact_type":"temperature_calibration","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/calibration.json","sha256":"sha256:422c930f7293464fef377f7992f08040ac086c67b161a0e0b12a05458f759c51","size":113,"uri":null},{"artifact_type":"golden_vectors","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/golden_vectors.json","sha256":"sha256:6d98be540e1d008882623ea3d4b210dc556824d6da1a8c6838451828fc843691","size":45147,"uri":null},{"artifact_type":"training_metrics","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/metrics.json","sha256":"sha256:4b7e7d724c21326b131ef307ea21bbe7f611e364054d598b27194e1f43cae482","size":3660,"uri":null},{"artifact_type":"lightgbm_model","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/tier/model.txt","sha256":"sha256:0a49539b8f26ca40945cfad01f32f84d40cfd3fedeceffd2480a0779db5dc8d1","size":28019262,"uri":null}],"outputs":{"intent":["new_task","continue","redo"],"tier":["C0","C1","C2","C3"]},"preprocess":[{"artifact_type":"preprocess_copy_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/preprocess_copy_manifest.json","sha256":"sha256:907a9b6f2ebdfad491fc98b3df13d4e8575077e581919e7c81f5a90847dde299","size":5140,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null},{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null},{"artifact_type":"feature_registry","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null},{"artifact_type":"transformer_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null},{"artifact_type":"feature_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature_manifest.json","sha256":"sha256:a2b66a973653464652d9a7ffd0da25a384b5bf8fa0f11dbe8a3b0c3d1205d5eb","size":4879,"uri":null}],"runtime":{"engine":"lightgbm","feature_dimension":380,"feature_version":"lightgbm_380.v1","split_hash":"sha256:89eccc5d9b221c15004b146417a7226b162ac0dceef5bf69457302f835c8a917"},"schema_version":"model_manifest.v1","status":"CANDIDATE","training_runs":{"intent":"lgbm-99d6ff75-d444135-a1-intent","tier":"lgbm-99d6ff75-d444135-a1-tier"}} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_recipe.json b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_recipe.json new file mode 100644 index 0000000000..da8223376a --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_recipe.json @@ -0,0 +1 @@ +{"environment":{"implementation":"CPython","machine":"x86_64","packages":{"joblib":"1.4.2","numpy":"1.26.4","onnxruntime":"1.23.2","scikit-learn":"1.8.0","tokenizers":"0.21.4"},"python":"3.11.11","system":"Linux"},"feature_version":"lightgbm_380.v1","parameters":{"source":"routerctl.feature.build-lightgbm"},"runtime":{"dtype":"float32","empty_semantic_segment":"zero64_and_bypass_encoder","encoder":{"batch_size":1,"inference_runtime":{"onnxruntime_build_info":"ORT Build Info: git-branch=HEAD, git-commit-id=a83fc4d58c, build type=Release","onnxruntime_version":"1.23.2","provider_options":{"CPUExecutionProvider":{}},"providers":["CPUExecutionProvider"],"schema_version":"bge_onnx_runtime_identity.v1","session_options":{"enable_cpu_mem_arena":true,"enable_mem_pattern":true,"enable_mem_reuse":true,"execution_mode":"ORT_SEQUENTIAL","execution_order":"DEFAULT","graph_optimization_level":"ORT_ENABLE_ALL","inter_op_num_threads":1,"intra_op_num_threads":1,"use_deterministic_compute":false},"session_type":"onnxruntime.capi.onnxruntime_inference_collection.InferenceSession","source":"onnxruntime"},"max_content_tokens":510,"model_file":"model.onnx","model_sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","normalization":"l2","pooling":"cls","postprocess_runtime":{"hidden_cast":"float32","normalization":"numpy.linalg.norm_l2_float32","numpy_version":"1.26.4","pooling":"cls","schema_version":"bge_postprocess_identity.v1"},"segment_truncation":{"current_request":"head_tail_3_to_1","previous_answer":"head_tail_1_to_1","recent_history":"tail"},"tokenizer_file":"tokenizer.json","tokenizer_sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","type":"bge_onnx"},"feature_dim":380,"pca":{"fitted_components":64,"output_dim":64,"svd_solver":"full","type":"sklearn.decomposition.PCA"},"schema_version":"lightgbm380_runtime.v1","svd":{"algorithm":"randomized","fitted_components":100,"n_iter":7,"output_dim":100,"random_state":17,"type":"sklearn.decomposition.TruncatedSVD"},"tfidf":{"analyzer":"char_wb","dtype":"float32","max_features":10000,"ngram_range":[2,4],"type":"sklearn.feature_extraction.text.TfidfVectorizer"}},"schema_version":"lightgbm_380_recipe.v1","status":"PUBLISHED"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_registry.json b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_registry.json new file mode 100644 index 0000000000..df641a08e1 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/feature_registry.json @@ -0,0 +1 @@ +{"dimension":380,"dtype":"float32","features":[{"index":0,"name":"current.semantic.000"},{"index":1,"name":"current.semantic.001"},{"index":2,"name":"current.semantic.002"},{"index":3,"name":"current.semantic.003"},{"index":4,"name":"current.semantic.004"},{"index":5,"name":"current.semantic.005"},{"index":6,"name":"current.semantic.006"},{"index":7,"name":"current.semantic.007"},{"index":8,"name":"current.semantic.008"},{"index":9,"name":"current.semantic.009"},{"index":10,"name":"current.semantic.010"},{"index":11,"name":"current.semantic.011"},{"index":12,"name":"current.semantic.012"},{"index":13,"name":"current.semantic.013"},{"index":14,"name":"current.semantic.014"},{"index":15,"name":"current.semantic.015"},{"index":16,"name":"current.semantic.016"},{"index":17,"name":"current.semantic.017"},{"index":18,"name":"current.semantic.018"},{"index":19,"name":"current.semantic.019"},{"index":20,"name":"current.semantic.020"},{"index":21,"name":"current.semantic.021"},{"index":22,"name":"current.semantic.022"},{"index":23,"name":"current.semantic.023"},{"index":24,"name":"current.semantic.024"},{"index":25,"name":"current.semantic.025"},{"index":26,"name":"current.semantic.026"},{"index":27,"name":"current.semantic.027"},{"index":28,"name":"current.semantic.028"},{"index":29,"name":"current.semantic.029"},{"index":30,"name":"current.semantic.030"},{"index":31,"name":"current.semantic.031"},{"index":32,"name":"current.semantic.032"},{"index":33,"name":"current.semantic.033"},{"index":34,"name":"current.semantic.034"},{"index":35,"name":"current.semantic.035"},{"index":36,"name":"current.semantic.036"},{"index":37,"name":"current.semantic.037"},{"index":38,"name":"current.semantic.038"},{"index":39,"name":"current.semantic.039"},{"index":40,"name":"current.semantic.040"},{"index":41,"name":"current.semantic.041"},{"index":42,"name":"current.semantic.042"},{"index":43,"name":"current.semantic.043"},{"index":44,"name":"current.semantic.044"},{"index":45,"name":"current.semantic.045"},{"index":46,"name":"current.semantic.046"},{"index":47,"name":"current.semantic.047"},{"index":48,"name":"current.semantic.048"},{"index":49,"name":"current.semantic.049"},{"index":50,"name":"current.semantic.050"},{"index":51,"name":"current.semantic.051"},{"index":52,"name":"current.semantic.052"},{"index":53,"name":"current.semantic.053"},{"index":54,"name":"current.semantic.054"},{"index":55,"name":"current.semantic.055"},{"index":56,"name":"current.semantic.056"},{"index":57,"name":"current.semantic.057"},{"index":58,"name":"current.semantic.058"},{"index":59,"name":"current.semantic.059"},{"index":60,"name":"current.semantic.060"},{"index":61,"name":"current.semantic.061"},{"index":62,"name":"current.semantic.062"},{"index":63,"name":"current.semantic.063"},{"index":64,"name":"current.character.000"},{"index":65,"name":"current.character.001"},{"index":66,"name":"current.character.002"},{"index":67,"name":"current.character.003"},{"index":68,"name":"current.character.004"},{"index":69,"name":"current.character.005"},{"index":70,"name":"current.character.006"},{"index":71,"name":"current.character.007"},{"index":72,"name":"current.character.008"},{"index":73,"name":"current.character.009"},{"index":74,"name":"current.character.010"},{"index":75,"name":"current.character.011"},{"index":76,"name":"current.character.012"},{"index":77,"name":"current.character.013"},{"index":78,"name":"current.character.014"},{"index":79,"name":"current.character.015"},{"index":80,"name":"current.character.016"},{"index":81,"name":"current.character.017"},{"index":82,"name":"current.character.018"},{"index":83,"name":"current.character.019"},{"index":84,"name":"current.character.020"},{"index":85,"name":"current.character.021"},{"index":86,"name":"current.character.022"},{"index":87,"name":"current.character.023"},{"index":88,"name":"current.character.024"},{"index":89,"name":"current.character.025"},{"index":90,"name":"current.character.026"},{"index":91,"name":"current.character.027"},{"index":92,"name":"current.character.028"},{"index":93,"name":"current.character.029"},{"index":94,"name":"current.character.030"},{"index":95,"name":"current.character.031"},{"index":96,"name":"current.character.032"},{"index":97,"name":"current.character.033"},{"index":98,"name":"current.character.034"},{"index":99,"name":"current.character.035"},{"index":100,"name":"current.character.036"},{"index":101,"name":"current.character.037"},{"index":102,"name":"current.character.038"},{"index":103,"name":"current.character.039"},{"index":104,"name":"current.character.040"},{"index":105,"name":"current.character.041"},{"index":106,"name":"current.character.042"},{"index":107,"name":"current.character.043"},{"index":108,"name":"current.character.044"},{"index":109,"name":"current.character.045"},{"index":110,"name":"current.character.046"},{"index":111,"name":"current.character.047"},{"index":112,"name":"current.character.048"},{"index":113,"name":"current.character.049"},{"index":114,"name":"current.character.050"},{"index":115,"name":"current.character.051"},{"index":116,"name":"current.character.052"},{"index":117,"name":"current.character.053"},{"index":118,"name":"current.character.054"},{"index":119,"name":"current.character.055"},{"index":120,"name":"current.character.056"},{"index":121,"name":"current.character.057"},{"index":122,"name":"current.character.058"},{"index":123,"name":"current.character.059"},{"index":124,"name":"current.character.060"},{"index":125,"name":"current.character.061"},{"index":126,"name":"current.character.062"},{"index":127,"name":"current.character.063"},{"index":128,"name":"current.character.064"},{"index":129,"name":"current.character.065"},{"index":130,"name":"current.character.066"},{"index":131,"name":"current.character.067"},{"index":132,"name":"current.character.068"},{"index":133,"name":"current.character.069"},{"index":134,"name":"current.character.070"},{"index":135,"name":"current.character.071"},{"index":136,"name":"current.character.072"},{"index":137,"name":"current.character.073"},{"index":138,"name":"current.character.074"},{"index":139,"name":"current.character.075"},{"index":140,"name":"current.character.076"},{"index":141,"name":"current.character.077"},{"index":142,"name":"current.character.078"},{"index":143,"name":"current.character.079"},{"index":144,"name":"current.character.080"},{"index":145,"name":"current.character.081"},{"index":146,"name":"current.character.082"},{"index":147,"name":"current.character.083"},{"index":148,"name":"current.character.084"},{"index":149,"name":"current.character.085"},{"index":150,"name":"current.character.086"},{"index":151,"name":"current.character.087"},{"index":152,"name":"current.character.088"},{"index":153,"name":"current.character.089"},{"index":154,"name":"current.character.090"},{"index":155,"name":"current.character.091"},{"index":156,"name":"current.character.092"},{"index":157,"name":"current.character.093"},{"index":158,"name":"current.character.094"},{"index":159,"name":"current.character.095"},{"index":160,"name":"current.character.096"},{"index":161,"name":"current.character.097"},{"index":162,"name":"current.character.098"},{"index":163,"name":"current.character.099"},{"index":164,"name":"current.rule.char_count"},{"index":165,"name":"current.rule.word_count"},{"index":166,"name":"current.rule.line_count"},{"index":167,"name":"current.rule.avg_chars_per_line"},{"index":168,"name":"current.rule.zh_char_ratio"},{"index":169,"name":"current.rule.latin_char_ratio"},{"index":170,"name":"current.rule.code_char_ratio"},{"index":171,"name":"current.rule.mixed_zh_en"},{"index":172,"name":"current.rule.has_code_block"},{"index":173,"name":"current.rule.code_block_count"},{"index":174,"name":"current.rule.code_block_chars"},{"index":175,"name":"current.rule.has_json"},{"index":176,"name":"current.rule.has_yaml"},{"index":177,"name":"current.rule.has_csv"},{"index":178,"name":"current.rule.has_table"},{"index":179,"name":"current.rule.question_mark_count"},{"index":180,"name":"current.rule.exclamation_mark_count"},{"index":181,"name":"current.rule.bullet_count"},{"index":182,"name":"current.rule.numbered_list_count"},{"index":183,"name":"current.rule.debug_keyword_count"},{"index":184,"name":"current.rule.research_keyword_count"},{"index":185,"name":"current.rule.architecture_keyword_count"},{"index":186,"name":"current.rule.compare_keyword_count"},{"index":187,"name":"current.rule.planning_keyword_count"},{"index":188,"name":"current.rule.strict_format_keyword_count"},{"index":189,"name":"current.rule.high_risk_keyword_count"},{"index":190,"name":"current.rule.production_keyword_count"},{"index":191,"name":"current.rule.customer_keyword_count"},{"index":192,"name":"current.rule.delete_keyword_count"},{"index":193,"name":"current.rule.formal_keyword_count"},{"index":194,"name":"current.rule.has_file_path"},{"index":195,"name":"current.rule.has_url"},{"index":196,"name":"current.rule.has_log"},{"index":197,"name":"current.rule.has_shell"},{"index":198,"name":"current.rule.has_traceback"},{"index":199,"name":"current.rule.constraint_keyword_count"},{"index":200,"name":"current.rule.quoted_text_ratio"},{"index":201,"name":"current.rule.word_diversity"},{"index":202,"name":"current.rule.teaching_keyword_count"},{"index":203,"name":"current.rule.implementation_keyword_count"},{"index":204,"name":"current.rule.file_count_zero"},{"index":205,"name":"current.rule.file_count_one_or_two"},{"index":206,"name":"current.rule.file_count_three_plus"},{"index":207,"name":"current.rule.code_without_debug"},{"index":208,"name":"current.rule.length_under_200"},{"index":209,"name":"current.rule.length_200_to_1000"},{"index":210,"name":"current.rule.length_over_1000"},{"index":211,"name":"current.rule.low_keyword_density"},{"index":212,"name":"history.semantic.000"},{"index":213,"name":"history.semantic.001"},{"index":214,"name":"history.semantic.002"},{"index":215,"name":"history.semantic.003"},{"index":216,"name":"history.semantic.004"},{"index":217,"name":"history.semantic.005"},{"index":218,"name":"history.semantic.006"},{"index":219,"name":"history.semantic.007"},{"index":220,"name":"history.semantic.008"},{"index":221,"name":"history.semantic.009"},{"index":222,"name":"history.semantic.010"},{"index":223,"name":"history.semantic.011"},{"index":224,"name":"history.semantic.012"},{"index":225,"name":"history.semantic.013"},{"index":226,"name":"history.semantic.014"},{"index":227,"name":"history.semantic.015"},{"index":228,"name":"history.semantic.016"},{"index":229,"name":"history.semantic.017"},{"index":230,"name":"history.semantic.018"},{"index":231,"name":"history.semantic.019"},{"index":232,"name":"history.semantic.020"},{"index":233,"name":"history.semantic.021"},{"index":234,"name":"history.semantic.022"},{"index":235,"name":"history.semantic.023"},{"index":236,"name":"history.semantic.024"},{"index":237,"name":"history.semantic.025"},{"index":238,"name":"history.semantic.026"},{"index":239,"name":"history.semantic.027"},{"index":240,"name":"history.semantic.028"},{"index":241,"name":"history.semantic.029"},{"index":242,"name":"history.semantic.030"},{"index":243,"name":"history.semantic.031"},{"index":244,"name":"history.semantic.032"},{"index":245,"name":"history.semantic.033"},{"index":246,"name":"history.semantic.034"},{"index":247,"name":"history.semantic.035"},{"index":248,"name":"history.semantic.036"},{"index":249,"name":"history.semantic.037"},{"index":250,"name":"history.semantic.038"},{"index":251,"name":"history.semantic.039"},{"index":252,"name":"history.semantic.040"},{"index":253,"name":"history.semantic.041"},{"index":254,"name":"history.semantic.042"},{"index":255,"name":"history.semantic.043"},{"index":256,"name":"history.semantic.044"},{"index":257,"name":"history.semantic.045"},{"index":258,"name":"history.semantic.046"},{"index":259,"name":"history.semantic.047"},{"index":260,"name":"history.semantic.048"},{"index":261,"name":"history.semantic.049"},{"index":262,"name":"history.semantic.050"},{"index":263,"name":"history.semantic.051"},{"index":264,"name":"history.semantic.052"},{"index":265,"name":"history.semantic.053"},{"index":266,"name":"history.semantic.054"},{"index":267,"name":"history.semantic.055"},{"index":268,"name":"history.semantic.056"},{"index":269,"name":"history.semantic.057"},{"index":270,"name":"history.semantic.058"},{"index":271,"name":"history.semantic.059"},{"index":272,"name":"history.semantic.060"},{"index":273,"name":"history.semantic.061"},{"index":274,"name":"history.semantic.062"},{"index":275,"name":"history.semantic.063"},{"index":276,"name":"history.route.previous_tier_index"},{"index":277,"name":"history.route.previous_difficulty"},{"index":278,"name":"history.route.previous_margin"},{"index":279,"name":"history.route.highest_tier_index"},{"index":280,"name":"history.route.turn_index"},{"index":281,"name":"history.route.route_history_length"},{"index":282,"name":"history.route.dominant_tier_index"},{"index":283,"name":"history.route.route_switch_count"},{"index":284,"name":"history.route.trajectory_cold_start"},{"index":285,"name":"history.route.trajectory_stable_low"},{"index":286,"name":"history.route.trajectory_stable_high"},{"index":287,"name":"history.route.trajectory_escalating"},{"index":288,"name":"history.route.trajectory_descalating"},{"index":289,"name":"history.route.trajectory_oscillating"},{"index":290,"name":"history.route.trajectory_unclear"},{"index":291,"name":"history.route.trajectory_mixed"},{"index":292,"name":"previous.semantic.000"},{"index":293,"name":"previous.semantic.001"},{"index":294,"name":"previous.semantic.002"},{"index":295,"name":"previous.semantic.003"},{"index":296,"name":"previous.semantic.004"},{"index":297,"name":"previous.semantic.005"},{"index":298,"name":"previous.semantic.006"},{"index":299,"name":"previous.semantic.007"},{"index":300,"name":"previous.semantic.008"},{"index":301,"name":"previous.semantic.009"},{"index":302,"name":"previous.semantic.010"},{"index":303,"name":"previous.semantic.011"},{"index":304,"name":"previous.semantic.012"},{"index":305,"name":"previous.semantic.013"},{"index":306,"name":"previous.semantic.014"},{"index":307,"name":"previous.semantic.015"},{"index":308,"name":"previous.semantic.016"},{"index":309,"name":"previous.semantic.017"},{"index":310,"name":"previous.semantic.018"},{"index":311,"name":"previous.semantic.019"},{"index":312,"name":"previous.semantic.020"},{"index":313,"name":"previous.semantic.021"},{"index":314,"name":"previous.semantic.022"},{"index":315,"name":"previous.semantic.023"},{"index":316,"name":"previous.semantic.024"},{"index":317,"name":"previous.semantic.025"},{"index":318,"name":"previous.semantic.026"},{"index":319,"name":"previous.semantic.027"},{"index":320,"name":"previous.semantic.028"},{"index":321,"name":"previous.semantic.029"},{"index":322,"name":"previous.semantic.030"},{"index":323,"name":"previous.semantic.031"},{"index":324,"name":"previous.semantic.032"},{"index":325,"name":"previous.semantic.033"},{"index":326,"name":"previous.semantic.034"},{"index":327,"name":"previous.semantic.035"},{"index":328,"name":"previous.semantic.036"},{"index":329,"name":"previous.semantic.037"},{"index":330,"name":"previous.semantic.038"},{"index":331,"name":"previous.semantic.039"},{"index":332,"name":"previous.semantic.040"},{"index":333,"name":"previous.semantic.041"},{"index":334,"name":"previous.semantic.042"},{"index":335,"name":"previous.semantic.043"},{"index":336,"name":"previous.semantic.044"},{"index":337,"name":"previous.semantic.045"},{"index":338,"name":"previous.semantic.046"},{"index":339,"name":"previous.semantic.047"},{"index":340,"name":"previous.semantic.048"},{"index":341,"name":"previous.semantic.049"},{"index":342,"name":"previous.semantic.050"},{"index":343,"name":"previous.semantic.051"},{"index":344,"name":"previous.semantic.052"},{"index":345,"name":"previous.semantic.053"},{"index":346,"name":"previous.semantic.054"},{"index":347,"name":"previous.semantic.055"},{"index":348,"name":"previous.semantic.056"},{"index":349,"name":"previous.semantic.057"},{"index":350,"name":"previous.semantic.058"},{"index":351,"name":"previous.semantic.059"},{"index":352,"name":"previous.semantic.060"},{"index":353,"name":"previous.semantic.061"},{"index":354,"name":"previous.semantic.062"},{"index":355,"name":"previous.semantic.063"},{"index":356,"name":"previous.signal.has_previous_answer"},{"index":357,"name":"previous.signal.previous_answer_clarifies"},{"index":358,"name":"previous.signal.previous_answer_refuses"},{"index":359,"name":"previous.signal.previous_answer_self_doubt"},{"index":360,"name":"previous.signal.previous_answer_has_code"},{"index":361,"name":"previous.signal.previous_answer_has_steps"},{"index":362,"name":"previous.signal.previous_output_tokens_log"},{"index":363,"name":"previous.signal.previous_reasoning_tokens_log"},{"index":364,"name":"previous.signal.previous_duration_log"},{"index":365,"name":"previous.signal.answer_to_request_length_ratio"},{"index":366,"name":"previous.signal.previous_answer_zh_ratio"},{"index":367,"name":"previous.signal.previous_cache_ratio"},{"index":368,"name":"previous.signal.short_continue_cue"},{"index":369,"name":"previous.signal.previous_output_tokens_log_repeat"},{"index":370,"name":"previous.signal.reasoning_cue"},{"index":371,"name":"previous.signal.question_density"},{"index":372,"name":"previous.signal.request_length_log"},{"index":373,"name":"previous.signal.previous_reasoning_tokens_log_repeat"},{"index":374,"name":"previous.signal.previous_duration_log_repeat"},{"index":375,"name":"context.turn_index_normalized"},{"index":376,"name":"context.context_tokens_normalized"},{"index":377,"name":"context.has_code_block"},{"index":378,"name":"context.is_deep_conversation"},{"index":379,"name":"context.is_heavy_context"}],"schema_version":"lightgbm_380.v1"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/pca64.joblib b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/pca64.joblib new file mode 100644 index 0000000000..9f57e315f5 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/pca64.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4 +size 135471 diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/runtime_manifest.json b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/runtime_manifest.json new file mode 100644 index 0000000000..32911f3936 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/runtime_manifest.json @@ -0,0 +1 @@ +{"encoder_asset_schema_version":"bge_encoder_assets.v1","encoder_assets":[{"artifact_type":"bge_onnx_model","path":"bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","role":"onnx_model","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398},{"artifact_type":"bge_tokenizer","path":"bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","role":"tokenizer","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125}],"files":[{"path":"pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471},{"path":"svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047},{"path":"tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451}],"metadata":{"dtype":"float32","empty_semantic_segment":"zero64_and_bypass_encoder","encoder":{"batch_size":1,"inference_runtime":{"onnxruntime_build_info":"ORT Build Info: git-branch=HEAD, git-commit-id=a83fc4d58c, build type=Release","onnxruntime_version":"1.23.2","provider_options":{"CPUExecutionProvider":{}},"providers":["CPUExecutionProvider"],"schema_version":"bge_onnx_runtime_identity.v1","session_options":{"enable_cpu_mem_arena":true,"enable_mem_pattern":true,"enable_mem_reuse":true,"execution_mode":"ORT_SEQUENTIAL","execution_order":"DEFAULT","graph_optimization_level":"ORT_ENABLE_ALL","inter_op_num_threads":1,"intra_op_num_threads":1,"use_deterministic_compute":false},"session_type":"onnxruntime.capi.onnxruntime_inference_collection.InferenceSession","source":"onnxruntime"},"max_content_tokens":510,"model_file":"model.onnx","model_sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","normalization":"l2","pooling":"cls","postprocess_runtime":{"hidden_cast":"float32","normalization":"numpy.linalg.norm_l2_float32","numpy_version":"1.26.4","pooling":"cls","schema_version":"bge_postprocess_identity.v1"},"segment_truncation":{"current_request":"head_tail_3_to_1","previous_answer":"head_tail_1_to_1","recent_history":"tail"},"tokenizer_file":"tokenizer.json","tokenizer_sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","type":"bge_onnx"},"feature_dim":380,"pca":{"fitted_components":64,"output_dim":64,"svd_solver":"full","type":"sklearn.decomposition.PCA"},"schema_version":"lightgbm380_runtime.v1","svd":{"algorithm":"randomized","fitted_components":100,"n_iter":7,"output_dim":100,"random_state":17,"type":"sklearn.decomposition.TruncatedSVD"},"tfidf":{"analyzer":"char_wb","dtype":"float32","max_features":10000,"ngram_range":[2,4],"type":"sklearn.feature_extraction.text.TfidfVectorizer"}},"schema_version":"lightgbm380_runtime.v1"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/svd100.joblib b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/svd100.joblib new file mode 100644 index 0000000000..5533086fab --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/svd100.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc +size 4002047 diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/tfidf_char_wb_2_4.joblib b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/tfidf_char_wb_2_4.joblib new file mode 100644 index 0000000000..a6611c8a8b --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature/artifacts/tfidf_char_wb_2_4.joblib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312 +size 298451 diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature_manifest.json b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature_manifest.json new file mode 100644 index 0000000000..b2ce99dd2b --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/feature_manifest.json @@ -0,0 +1 @@ +{"artifacts":[{"artifact_type":"feature_artifact","path":"artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null},{"artifact_type":"feature_artifact","path":"artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null},{"artifact_type":"feature_artifact","path":"artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null},{"artifact_type":"feature_registry","path":"artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null},{"artifact_type":"transformer_manifest","path":"artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null},{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null},{"artifact_type":"feature_data_dev","path":"data/dev/part-00000.parquet","sha256":"sha256:a19bd80db5b4966e4aef412db1115f10502a6e4da40514f23ae2464f005f89c9","size":15062586,"uri":null},{"artifact_type":"feature_data_test","path":"data/test/part-00000.parquet","sha256":"sha256:e5997778b5dbd16daa4fdac3c2adf2130ed0d3ef5c8054e135ee1fb1181b7054","size":14828420,"uri":null},{"artifact_type":"feature_data_train","path":"data/train/part-00000.parquet","sha256":"sha256:18e6ab3097e35e09b4723b5814c4114c85fef7d37349b49eb35b753d3bda5b32","size":114990823,"uri":null}],"code_digest":"git:d4441356584423be07361495e55b488c95c5db43","dataset_manifest_hash":"sha256:a190a4a154d493631f1811bfaac7ce14b6d23c3104af743b6fd605bb6fc0d609","dataset_version":"router_training.2026-07-13.1+99d6ff75","feature_recipe_hash":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","feature_version":"lightgbm_380.v1","feature_view_id":"sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c","sample_id_set_hash":"sha256:4493eca28ccdce3a0490c214134dfba2aa053c652ae045cb882c0f1e79535aa8","schema_version":"feature_manifest.v1","splits_hash":"sha256:89eccc5d9b221c15004b146417a7226b162ac0dceef5bf69457302f835c8a917","status":"PUBLISHED","train_split_hash":"sha256:39f20796dcab560da04bd52610b8c2f5aa042732ec82f908bf133caa7c674485","transformers":{"dtype":"float32","empty_semantic_segment":"zero64_and_bypass_encoder","encoder":{"batch_size":1,"inference_runtime":{"onnxruntime_build_info":"ORT Build Info: git-branch=HEAD, git-commit-id=a83fc4d58c, build type=Release","onnxruntime_version":"1.23.2","provider_options":{"CPUExecutionProvider":{}},"providers":["CPUExecutionProvider"],"schema_version":"bge_onnx_runtime_identity.v1","session_options":{"enable_cpu_mem_arena":true,"enable_mem_pattern":true,"enable_mem_reuse":true,"execution_mode":"ORT_SEQUENTIAL","execution_order":"DEFAULT","graph_optimization_level":"ORT_ENABLE_ALL","inter_op_num_threads":1,"intra_op_num_threads":1,"use_deterministic_compute":false},"session_type":"onnxruntime.capi.onnxruntime_inference_collection.InferenceSession","source":"onnxruntime"},"max_content_tokens":510,"model_file":"model.onnx","model_sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","normalization":"l2","pooling":"cls","postprocess_runtime":{"hidden_cast":"float32","normalization":"numpy.linalg.norm_l2_float32","numpy_version":"1.26.4","pooling":"cls","schema_version":"bge_postprocess_identity.v1"},"segment_truncation":{"current_request":"head_tail_3_to_1","previous_answer":"head_tail_1_to_1","recent_history":"tail"},"tokenizer_file":"tokenizer.json","tokenizer_sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","type":"bge_onnx"},"feature_dim":380,"pca":{"fitted_components":64,"output_dim":64,"svd_solver":"full","type":"sklearn.decomposition.PCA"},"schema_version":"lightgbm380_runtime.v1","svd":{"algorithm":"randomized","fitted_components":100,"n_iter":7,"output_dim":100,"random_state":17,"type":"sklearn.decomposition.TruncatedSVD"},"tfidf":{"analyzer":"char_wb","dtype":"float32","max_features":10000,"ngram_range":[2,4],"type":"sklearn.feature_extraction.text.TfidfVectorizer"}}} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/preprocess_copy_manifest.json b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/preprocess_copy_manifest.json new file mode 100644 index 0000000000..fb1d595a8b --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/preprocess/preprocess_copy_manifest.json @@ -0,0 +1 @@ +{"extra_artifacts":[],"feature_artifacts":[{"published":{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null},"source":{"artifact_type":"feature_artifact","path":"artifacts/bge/onnx_model/sha256/87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d.onnx","sha256":"sha256:87847793fac866d65b19cee2e73a6b2a2446a1262ab0911b1dbf9b3e9399ef9d","size":23902398,"uri":null}},{"published":{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null},"source":{"artifact_type":"feature_artifact","path":"artifacts/bge/tokenizer/sha256/48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26.json","sha256":"sha256:48cea5d44424912a6fd1ea647bf4fe50b55ab8b1e5879c3275f80e339e8fae26","size":439125,"uri":null}},{"published":{"artifact_type":"feature_artifact","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null},"source":{"artifact_type":"feature_artifact","path":"artifacts/feature_recipe.json","sha256":"sha256:c3e88bec2caec485d0d74f13d5654404c6b35f2a891564f1460fb6d32ae6d55d","size":2249,"uri":null}},{"published":{"artifact_type":"feature_registry","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null},"source":{"artifact_type":"feature_registry","path":"artifacts/feature_registry.json","sha256":"sha256:e72fa3d58e23392484e60d69e771840ae4239d4d0976d326af8d3462a26c5e17","size":17937,"uri":null}},{"published":{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null},"source":{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/pca64.joblib","sha256":"sha256:86dafb29f5fe068b4908db09e7dd4bb75a907cce9826d7c4648660dda16b78b4","size":135471,"uri":null}},{"published":{"artifact_type":"transformer_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null},"source":{"artifact_type":"transformer_manifest","path":"artifacts/runtime_manifest.json","sha256":"sha256:2cfd622f10674cf336f3bac46f65ae21c9694933abbab0ec42bf8e324321523c","size":2887,"uri":null}},{"published":{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null},"source":{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/svd100.joblib","sha256":"sha256:76ecc02ff3dfa290a7433a5bb3fcdbe0a03945239d5bfff9cc4e5a1c6b6f30dc","size":4002047,"uri":null}},{"published":{"artifact_type":"trusted_sklearn_transformer","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature/artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null},"source":{"artifact_type":"trusted_sklearn_transformer","path":"artifacts/tfidf_char_wb_2_4.joblib","sha256":"sha256:95c8546d13686519c07c0387687e5146727e9eef822b4114ec629153a85f1312","size":298451,"uri":null}}],"feature_manifest":{"artifact_type":"feature_manifest","path":"models/router-training/runs/lgbm-99d6ff75-d444135-a1/preprocess/sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c/feature_manifest.json","sha256":"sha256:a2b66a973653464652d9a7ffd0da25a384b5bf8fa0f11dbe8a3b0c3d1205d5eb","size":4879,"uri":null},"feature_view_id":"sha256:8a636e81171255a0b09e242ab2ddef178363abc6c230d95604aec153fc4f825c","schema_version":"preprocess_copy_manifest.v1"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/provenance.json b/src/opensquilla/squilla_router/lightgbm_model/assets/provenance.json new file mode 100644 index 0000000000..2267f301db --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/provenance.json @@ -0,0 +1,43 @@ +{ + "source_snapshot": "tier-policy-ablation-20261020/router-training-a34e5ae", + "training_code_digest": "git:d4441356584423be07361495e55b488c95c5db43", + "model_set_id": "router-lightgbm-99d6ff75-d444135-a1", + "manifest_hash": "sha256:29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357", + "source_files": [ + { + "path": "src/router_training/contracts.py", + "sha256": "sha256:b0b6a879e6e8fffae69ecb138c381851a2e7b74c2a5fc769d986b91fd9609d6e" + }, + { + "path": "src/router_training/features/__init__.py", + "sha256": "sha256:03845e09ae6c2f74e8cdd6d3ca1adde4429ab4876f3402c75e7676a26fc9ff57" + }, + { + "path": "src/router_training/features/lightgbm380.py", + "sha256": "sha256:89588d0008e093b383188ed4c3ab944c436fadfeaba2814bece6d6ddbeb4831e" + }, + { + "path": "src/router_training/features/numeric.py", + "sha256": "sha256:ef327f463ce9851bb4c326aacf00b23f85f9268f47699f75703a67400a52a306" + }, + { + "path": "src/router_training/features/registry.py", + "sha256": "sha256:12da54112196ea054831fe76b206c138aad40673c9b38314f7243900d529d8d1" + }, + { + "path": "src/router_training/features/runtime.py", + "sha256": "sha256:821b8b0b24849373ff99f3fb9f1623761e787877e2057492f27f00f511ba4b5b" + }, + { + "path": "src/router_training/features/text.py", + "sha256": "sha256:c6fe801a56806aefc1c9bdbce29cdff8b36d5a49142d8682954488860e3a7885" + }, + { + "path": "src/router_training/training/common.py", + "sha256": "sha256:b6db10f1f12ad6308e3a5d0d74292a49c2b224a9d7be18256302b5f940a57f6e" + } + ], + "source_license": "LicenseRef-Proprietary (original pyproject.toml; no LICENSE file in frozen snapshot)", + "changes": "Vendored inference subset extracted from feature modules. Training, artifact export/loading, BERT, registry CLI, unused batch transforms, training-only transformer constructors, the redundant feature-runtime wrapper and unused encoder inspection/context-manager methods removed. The unused SQLite embedding cache and dedicated identity helpers removed; encoder construction, close and encode specialize the original memory-cache path by deleting unused persistence branches. Tokenization, ONNX execution, batching, ordering and normalization unchanged. Other retained function and method definitions preserve their original arithmetic; type-only annotations and casts were added for static checks. RouterInput/ContractModel and calibration arithmetic were extracted from the recorded upstream files. Loader resolves original artifact paths from a static manifest and remaps only the three frozen joblib wrapper class names; transforms and model bytes unchanged. The packaged model shares the byte-identical BGE model and tokenizer in models/v4.2_phase3_inference/bge_onnx. External bundles retain self-contained short filenames. Original source manifests and hashes are unchanged. Training run manifests/metrics omitted from inference package.", + "source_files_description": "Paths and SHA-256 hashes identify complete original upstream files, not the local inference extracts." +} diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/runtime_compatibility.json b/src/opensquilla/squilla_router/lightgbm_model/assets/runtime_compatibility.json new file mode 100644 index 0000000000..d993ba1c8e --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/runtime_compatibility.json @@ -0,0 +1,305 @@ +{ + "runtime_versions": { + "numpy": "2.5.3", + "lightgbm": "4.7.0", + "onnxruntime": "1.24.4", + "tokenizers": "0.23.2", + "scikit-learn": "1.8.0", + "joblib": "1.6.0" + }, + "warnings": [], + "cases": [ + { + "name": "cold_start_zh", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + }, + { + "name": "code_debug", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + }, + { + "name": "continue_with_history", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + }, + { + "name": "redo", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + }, + { + "name": "long_three_segments", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + }, + { + "name": "unicode_normalization", + "feature_max_abs": 0.0, + "feature_hash_equal": true, + "probability_max_abs": { + "intent": 0.0, + "tier": 0.0 + } + } + ], + "golden": { + "intent": { + "cases": 8, + "max_abs": 1.1102230246251565e-16 + }, + "tier": { + "cases": 8, + "max_abs": 2.220446049250313e-16 + } + }, + "cpu_validation": { + "host": "Intel Xeon 6982P-C", + "method": "The same pinned CPUExecutionProvider ran native and with CPUID capability masking in isolated processes. Masking VNNI/AMX retained AVX-512; masking AVX-512/VNNI/AMX selected AVX2. No source weights or golden values were changed.", + "session_config": { + "session.x64quantprecision": "1" + }, + "native_avx512_vnni": { + "cases": 6, + "feature_hashes_equal": true, + "probability_max_abs": 0.0 + }, + "avx512_without_vnni": { + "cases": 6, + "feature_hashes_equal": true, + "probability_max_abs": 0.0 + }, + "avx2_without_vnni": { + "other_cases": 5, + "other_feature_max_abs": 2.086162567138672e-07, + "other_probability_max_abs": 1.1102230246251565e-16, + "feature_max_abs": 0.0033130943775177, + "intent_probability_max_abs": 0.002630628685639236, + "tier_probability_max_abs": 0.006423068679724642, + "cause": "Floating-point CPU kernel differences cross DynamicQuantizeLinear half-integer rounding boundaries. Explicit U8U8 weights reproduce the same remaining difference, excluding a missed signed-weight conversion.", + "first_quantization_difference": { + "segment": 0, + "node": "/encoder/layer.1/attention/self/Reshape_3_output_0_quantized", + "index": [ + 0, + 342, + 83 + ], + "native": { + "float_input": 0.814724862575531, + "scale": 0.018308423459529877, + "zero_point": 125, + "input_div_scale_float32": 44.5, + "quantized": 169 + }, + "avx2": { + "float_input": 0.8147250413894653, + "scale": 0.018308419734239578, + "zero_point": 125, + "input_div_scale_float32": 44.50001907348633, + "quantized": 170 + }, + "input_abs_difference": 1.7881393432617188e-07 + }, + "largest_difference_case": "long_three_segments", + "source_case_labels": { + "cases": 6, + "intent_labels_equal": true, + "tier_labels_equal": true, + "policy_decisions_equal": true + }, + "classifier_golden_labels": { + "cases": 16, + "expected_head_labels_equal": true, + "native_both_head_labels_equal": true, + "probability_max_abs": 2.220446049250313e-16 + } + }, + "original_ci_failure": { + "case": "cold_start_zh", + "tier_probability_max_abs_without_precision_mode": 0.019415810857340232, + "avx2_probability_max_abs_with_precision_mode": 0.0, + "diagnosis": "All four CI probabilities reproduced exactly without precision mode. ONNX Runtime documents saturating U8S8 kernels on x64 without VNNI; precision mode performs an equivalent unsigned-weight conversion to avoid this overflow." + }, + "onnxruntime_reference": "https://github.com/microsoft/onnxruntime/blob/v1.24.4/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h#L157-L161", + "fixture_acceptance": { + "scope": "For x86, only long_three_segments uses the wider numerical bounds below. The other five source inputs retain their original strict bounds. ARM64-specific evidence and bounds are recorded separately below. All six always require exact labels and policy decisions; these are not bounds on arbitrary inputs or unmeasured CPU/runtime combinations.", + "semantic_feature_indices": [ + [ + 0, + 64 + ], + [ + 212, + 276 + ], + [ + 292, + 356 + ] + ], + "semantic_feature_absolute_tolerance": 0.0035, + "other_feature_relative_tolerance": 2e-05, + "other_feature_absolute_tolerance": 2e-06, + "intent_probability_absolute_tolerance": 0.003, + "tier_probability_absolute_tolerance": 0.007, + "probability_relative_tolerance": 0, + "exact_checks": [ + "intent_argmax", + "tier_argmax", + "policy_intent", + "policy_tier", + "policy_raw_tier", + "thinking_mode", + "prompt_policy", + "context_reset", + "next_state" + ], + "fixed_feature_classifier_golden": { + "cases": 16, + "relative_tolerance": 1e-10, + "absolute_tolerance": 1e-12 + }, + "rationale": "The long_three_segments bounds round its measured AVX2 maxima (semantic features 0.0033130944, intent probability 0.0026306287, tier probability 0.0064230687) upward modestly. All other fixtures, non-BGE features, and standalone classifier checks retain their original precision. Learned asset hashes and all original golden values remain pinned.", + "wider_bounds_case": "long_three_segments", + "other_five_full_pipeline_fixtures": { + "feature_relative_tolerance": 2e-05, + "feature_absolute_tolerance": 2e-06, + "probability_relative_tolerance": 1e-10, + "probability_absolute_tolerance": 1e-12 + } + }, + "apple_arm64": { + "host": "Apple M5", + "platform": "macOS-26.5.2-arm64-arm-64bit", + "machine": "arm64", + "method": "Native arm64 Python and CPUExecutionProvider with the same pinned runtime versions; no Rosetta. The full model test file passed on this host, native x86 AVX-512/VNNI, and CPUID-masked AVX2. Original source fixtures and learned bytes are unchanged.", + "session_config": { + "optimization.disable_specified_optimizers": "MatMulIntegerToFloatFusion;GeluFusionL2" + }, + "original_fused_graph_tier_probability_max_abs": 0.09544998406404825, + "configured_tier_probability_max_abs": 0.0028238415033203235, + "cases": [ + { + "name": "cold_start_zh", + "semantic_feature_max_abs": 5.960464477539063e-08, + "other_feature_max_abs": 1.4901161193847656e-08, + "intent_probability_max_abs": 0.0, + "tier_probability_max_abs": 0.0 + }, + { + "name": "code_debug", + "semantic_feature_max_abs": 0.00639711320400238, + "other_feature_max_abs": 2.9802322387695312e-08, + "intent_probability_max_abs": 0.0006391705050320384, + "tier_probability_max_abs": 0.0017489577329584949 + }, + { + "name": "continue_with_history", + "semantic_feature_max_abs": 5.21540641784668e-08, + "other_feature_max_abs": 7.450580596923828e-09, + "intent_probability_max_abs": 1.1102230246251565e-16, + "tier_probability_max_abs": 0.0 + }, + { + "name": "redo", + "semantic_feature_max_abs": 5.960464477539063e-08, + "other_feature_max_abs": 7.450580596923828e-09, + "intent_probability_max_abs": 0.0, + "tier_probability_max_abs": 0.0 + }, + { + "name": "long_three_segments", + "semantic_feature_max_abs": 0.00606517493724823, + "other_feature_max_abs": 1.862645149230957e-09, + "intent_probability_max_abs": 0.0014951235274058838, + "tier_probability_max_abs": 0.0028238415033203235 + }, + { + "name": "unicode_normalization", + "semantic_feature_max_abs": 5.21540641784668e-08, + "other_feature_max_abs": 2.9802322387695312e-08, + "intent_probability_max_abs": 1.1102230246251565e-16, + "tier_probability_max_abs": 0.0 + } + ], + "source_case_labels": { + "cases": 6, + "intent_labels_equal": true, + "tier_labels_equal": true, + "policy_decisions_equal": true + }, + "classifier_golden": { + "intent": { + "cases": 8, + "max_abs": 1.1102230246251565e-16 + }, + "tier": { + "cases": 8, + "max_abs": 2.220446049250313e-16 + } + }, + "first_quantization_difference": { + "case": "code_debug", + "graph": "ORT_ENABLE_BASIC with original weights and intermediate outputs", + "node": "/encoder/layer.1/attention/self/Reshape_3_output_0_quantized", + "index": [ + 0, + 380, + 221 + ], + "input_abs_difference": 1.4901161193847656e-08, + "quantized_mismatch_count": 1, + "x86_64": { + "float_input": 0.19614535570144653, + "scale": 0.02064688131213188, + "zero_point": 142, + "input_div_scale_float32": 9.499999046325684, + "quantized": 151 + }, + "arm64": { + "float_input": 0.19614537060260773, + "scale": 0.02064688131213188, + "zero_point": 142, + "input_div_scale_float32": 9.5, + "quantized": 152 + } + }, + "fixture_acceptance": { + "code_debug": { + "semantic_feature_absolute_tolerance": 0.0065, + "intent_probability_absolute_tolerance": 0.00065, + "tier_probability_absolute_tolerance": 0.0018 + }, + "long_three_segments": { + "semantic_feature_absolute_tolerance": 0.0062, + "intent_probability_absolute_tolerance": 0.003, + "tier_probability_absolute_tolerance": 0.007 + }, + "scope": "Only arm64/aarch64 and these two frozen inputs use these bounds. Non-BGE features, four other inputs, and all fixed-feature classifier checks retain their strict precision; all six require identical classifier labels, full policy decisions and next states. No accuracy guarantee for arbitrary inputs or unmeasured ARM kernels." + }, + "optimizer_name_reference": "https://github.com/microsoft/onnxruntime/blob/v1.24.4/onnxruntime/core/optimizer/gelu_fusion.h#L20-L27" + } + } +} diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/source_parity_cases.json b/src/opensquilla/squilla_router/lightgbm_model/assets/source_parity_cases.json new file mode 100644 index 0000000000..a33f126c38 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/source_parity_cases.json @@ -0,0 +1,2480 @@ +{ + "schema_version": "lightgbm_source_parity.v1", + "source_identity": { + "schema_version": "local_runner_identity.v2", + "model_set_id": "router-lightgbm-99d6ff75-d444135-a1", + "model_manifest_hash": "sha256:29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357", + "artifact_closure_hash": "sha256:f4913de4269c8c9bfdcdf977550b991975d4cf0cd86bb37d3cbc7d82a4304e07", + "runner_digest": "sha256:bf1c6cc3c27a708b27d4a418e1cc1b749a0416d7d0f928ba124e8d7fc97408c6", + "environment_digest": "sha256:65e6e49336acd540c2c2c73db6cd41e85679d75bf8ca33ce3b8f2b2ce919f1c7", + "model_type": "lightgbm", + "execution_mode": "native_embedded" + }, + "cases": [ + { + "name": "cold_start_zh", + "input": { + "current_request": "你好,请解释一下什么是二分查找。" + }, + "features": [ + -0.24677474796772003, + 0.2145904004573822, + 0.02596318908035755, + -0.14428313076496124, + 0.008997760713100433, + 0.1375202238559723, + 0.0007811747491359711, + 0.16316145658493042, + 0.0070157102309167385, + 0.05076099559664726, + -0.012447793036699295, + 0.03087640553712845, + 0.04071673005819321, + 0.06201039254665375, + -0.12275347858667374, + -0.05335550382733345, + -0.10596819967031479, + -0.08830495178699493, + -0.056410759687423706, + 0.04784122481942177, + 0.04479687660932541, + 0.06119468808174133, + -0.04780928045511246, + 0.012513600289821625, + 0.012578088790178299, + 0.005165576934814453, + 0.09677697718143463, + -0.029220204800367355, + -0.06501948833465576, + 0.029485613107681274, + 0.0723109319806099, + -0.0010929591953754425, + 0.020909752696752548, + 0.015441861003637314, + 0.07383621484041214, + -0.043288279324769974, + -0.10253618657588959, + -0.0016189143061637878, + 0.0603417232632637, + 0.03509029746055603, + -0.0018621496856212616, + 0.029246855527162552, + -0.04581082612276077, + -0.049697794020175934, + -0.002206815406680107, + 0.0029507242143154144, + 0.06468107551336288, + 0.029419809579849243, + -0.020348787307739258, + 0.05406908318400383, + 0.0005223602056503296, + -0.10119521617889404, + 0.07493410259485245, + -0.006724098697304726, + -0.015615358017385006, + -0.03878030553460121, + 0.017774581909179688, + -0.038771260529756546, + 0.03668681159615517, + 0.03484725207090378, + -0.009637154638767242, + -0.05213199928402901, + -0.0824919119477272, + 0.06842801719903946, + 0.009075839072465897, + 0.014611712656915188, + 0.01070871576666832, + 0.05189179256558418, + 0.04334498941898346, + 0.15710997581481934, + 0.052094846963882446, + 0.051964327692985535, + -0.003964926116168499, + 0.001821023877710104, + -0.0022036381997168064, + 0.017812175676226616, + 0.10407750308513641, + 0.13114246726036072, + 0.021834535524249077, + 0.005705885123461485, + 0.028656428679823875, + 0.044611841440200806, + 0.1148100346326828, + 0.05326419696211815, + -0.04565000906586647, + -0.017113056033849716, + 0.013199874199926853, + -0.04709077253937721, + -0.04940670356154442, + 0.10433544218540192, + -0.0008465335704386234, + 0.0018344959244132042, + 0.0002336646430194378, + -0.003983713686466217, + 0.013057658448815346, + 2.203858457505703e-05, + -0.021679215133190155, + 0.014555004425346851, + 0.015611316077411175, + -0.0025601638481020927, + 0.005471380893141031, + 0.007538242731243372, + 0.01793849654495716, + -0.0037778038531541824, + 0.0028556697070598602, + -0.029858317226171494, + -0.020297322422266006, + -0.0644637793302536, + -0.09762939065694809, + -0.13609981536865234, + -0.04061080515384674, + -0.1326451599597931, + -0.12089411914348602, + -0.01664133183658123, + -0.05328381061553955, + -0.05056405067443848, + 0.1697159856557846, + -0.043776266276836395, + 0.0735037624835968, + -0.017854813486337662, + 0.013928228057920933, + 0.009806063026189804, + 0.011156201362609863, + 0.07111144810914993, + 0.0057467324659228325, + 0.052359092980623245, + -0.012205660343170166, + 0.02486375905573368, + -0.0026597054675221443, + 0.016978606581687927, + -0.02270759642124176, + 0.008027821779251099, + 0.1675555258989334, + 0.11689739674329758, + -0.01841195672750473, + 0.16511595249176025, + -0.10172116756439209, + 0.01052684523165226, + 0.013684333302080631, + -0.14739462733268738, + -0.012701129540801048, + 0.05109000205993652, + 0.05831560119986534, + -0.0708555281162262, + -0.0588170662522316, + 0.041894346475601196, + -0.001941874623298645, + 0.036787886172533035, + 0.01013198122382164, + 0.07681722193956375, + 0.04476882517337799, + -0.018265873193740845, + 0.021787747740745544, + 0.00031661801040172577, + -0.005385659635066986, + -0.073676697909832, + -0.07507263869047165, + 0.0030996501445770264, + -0.027359075844287872, + 0.058405742049217224, + 0.060952406376600266, + -0.019216418266296387, + -0.0315372534096241, + 0.01452641747891903, + 16.0, + 1.0, + 1.0, + 16.0, + 0.875, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.2833213210105896, + 0.0, + 0.0, + 0.0, + 0.00019999999494757503, + 0.0, + 0.0, + 0.0 + ], + "feature_vector_hash": "sha256:6aad73d08529c3a253dc01809f8d709c41e903d235d4c938c38506d7fa34f322", + "intent_probabilities": { + "new_task": 0.9559918706623164, + "continue": 0.03845358875105275, + "redo": 0.0055545405866308266 + }, + "tier_probabilities": { + "C0": 0.686649205364739, + "C1": 0.08816536401916586, + "C2": 0.22117355773526334, + "C3": 0.004011872880831817 + } + }, + { + "name": "code_debug", + "input": { + "current_request": "Fix this bug and add a regression test:\n```python\ndef f(x):\n return x[1]\n```\nTraceback (most recent call last):\nIndexError: list index out of range" + }, + "features": [ + 0.07423558831214905, + -0.09263928234577179, + 0.23606786131858826, + -0.016556229442358017, + 0.11664025485515594, + 0.02671079896390438, + 0.06228163465857506, + 0.17516198754310608, + -0.02113169990479946, + 0.12436355650424957, + 0.03289717435836792, + 0.1411736011505127, + -0.06189480051398277, + 0.023943491280078888, + 0.004486612975597382, + 0.024030562490224838, + 0.10840427130460739, + 0.020648546516895294, + -0.0004630535840988159, + -0.0016423705965280533, + -0.012730695307254791, + 0.1476779580116272, + 0.02507760375738144, + -0.04676428437232971, + 0.08498281985521317, + -0.0860428661108017, + 0.042095836251974106, + 0.028235584497451782, + 0.006928912363946438, + -0.08751507103443146, + 0.04370564967393875, + 0.026075419038534164, + 0.021449854597449303, + 0.024432752281427383, + -0.02423204854130745, + -0.03962629288434982, + -0.04367513209581375, + 0.057972751557826996, + 0.007500514388084412, + 0.019884124398231506, + -0.017727281898260117, + 0.002008877694606781, + -0.02769402042031288, + 0.10763705521821976, + -0.01012546569108963, + -0.06253080070018768, + -0.026457207277417183, + 0.05057923123240471, + -0.013252649456262589, + 0.03478892892599106, + -0.021642934530973434, + 0.035887543112039566, + -0.024772202596068382, + -0.01855562999844551, + 0.013341234996914864, + 0.027116872370243073, + 0.004556339234113693, + -0.00015912950038909912, + 0.003313913941383362, + 0.037410348653793335, + 0.07607575505971909, + -0.06176551803946495, + -0.027596494182944298, + -0.05071025341749191, + 0.32662367820739746, + -0.055101681500673294, + -0.024133039638400078, + 0.0007050622953101993, + 0.016278740018606186, + 0.011733613908290863, + -0.04879765212535858, + 0.013635413721203804, + -0.02027927339076996, + 0.00025936480960808694, + 0.030102917924523354, + -0.016407640650868416, + 0.043282218277454376, + -0.023990420624613762, + -0.03161172941327095, + 0.005338291637599468, + -0.0015213096048682928, + 0.008193560875952244, + 0.027307383716106415, + -0.06817340850830078, + 0.0017920031677931547, + -0.023206926882267, + -0.09906032681465149, + 0.08159831166267395, + -0.0056730336509644985, + 0.002554592676460743, + -0.02110668458044529, + -0.1371115893125534, + 0.006010261829942465, + -0.006241466384381056, + 0.01300361193716526, + -0.06293793022632599, + 0.006065034307539463, + 0.03547486662864685, + -0.1083158403635025, + 0.10218613594770432, + -0.055287886410951614, + -0.06197252497076988, + -0.05349540337920189, + 0.113612100481987, + -0.023182380944490433, + -0.13301308453083038, + 0.04350857436656952, + 0.04663979262113571, + -0.08065781742334366, + 0.049412183463573456, + 0.0005526724271476269, + -0.009828007780015469, + 0.05125146359205246, + -0.031776104122400284, + 0.002760945586487651, + 0.06687568128108978, + 0.02167034149169922, + -0.012922564521431923, + -0.012395568192005157, + -0.03085682913661003, + 0.0020170288626104593, + 0.06826557964086533, + 0.028075916692614555, + -0.002258228836581111, + 0.03558303043246269, + -0.017182528972625732, + -0.020203107967972755, + -0.030991727486252785, + -0.06518245488405228, + -0.00997801311314106, + 0.04170650616288185, + 0.012364278547465801, + 0.03156736493110657, + -0.019579272717237473, + -0.009517387486994267, + 0.023912210017442703, + -0.033421095460653305, + -0.010123403742909431, + -0.01741892285645008, + -0.01952635496854782, + -0.004880132153630257, + -0.05135044828057289, + -0.016247684136033058, + 0.04782085493206978, + -0.019040215760469437, + -0.03663238510489464, + 0.02920793555676937, + 0.012125006876885891, + 0.0303557850420475, + 0.06678846478462219, + -0.050852466374635696, + -0.051355671137571335, + -0.04125940427184105, + -0.06726473569869995, + -0.04421557858586311, + -0.05125555396080017, + 0.00023831322323530912, + -0.04782533273100853, + -0.003945052623748779, + -0.04256174713373184, + 0.05173248052597046, + -0.00498343538492918, + 0.02459203451871872, + 0.03012196160852909, + 150.0, + 25.0, + 7.0, + 21.428571701049805, + 0.0, + 0.699999988079071, + 0.07999999821186066, + 0.0, + 1.0, + 1.0, + 39.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 3.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5017279982566833, + 0.0, + 0.0, + 0.0, + 0.0018500000005587935, + 1.0, + 0.0, + 0.0 + ], + "feature_vector_hash": "sha256:b48d9b9b67f7b87c216d163f20e39d35c08dbde2028d388b828822d86c2cc566", + "intent_probabilities": { + "new_task": 0.9942645517148944, + "continue": 0.005659132722996635, + "redo": 7.631556210896895e-05 + }, + "tier_probabilities": { + "C0": 0.4739910560772118, + "C1": 0.43073130080594824, + "C2": 0.0947783391838439, + "C3": 0.000499303932996005 + } + }, + { + "name": "continue_with_history", + "input": { + "current_request": "请继续,展开第二步。", + "task_anchor": "设计数据库迁移方案", + "history_user": [ + "请设计数据库迁移方案", + "要支持回滚并保留审计日志" + ], + "previous_answer": "1. 备份现有数据。\n2. 创建影子表。\n3. 验证迁移结果。", + "previous_usage": { + "input_tokens": 2048, + "output_tokens": 300, + "reasoning_tokens": 120, + "cached_tokens": 1000, + "duration_ms": 8500 + }, + "previous_outcome": "success", + "active_route_tier": "C2", + "route_history": [ + { + "tier": "C1", + "difficulty": 1.1, + "margin": 0.4 + }, + { + "tier": "C2", + "difficulty": 2.1, + "margin": 0.2 + } + ], + "context": { + "context_tokens_est": 4096 + } + }, + "features": [ + -0.24985742568969727, + 0.2977526783943176, + 0.012258479371666908, + -0.03992757573723793, + 0.0345589816570282, + 0.05891305208206177, + -0.0729992538690567, + -0.005927272140979767, + 0.012944839894771576, + 0.11443443596363068, + -0.0008336640894412994, + 0.039478033781051636, + 0.00573117658495903, + -0.10365915298461914, + -0.1230650469660759, + 0.019171323627233505, + -0.11038351058959961, + 0.1064811572432518, + 0.07502900063991547, + 0.00912809930741787, + -0.0252394862473011, + 0.048843663185834885, + -0.005433615297079086, + -0.010954562574625015, + -0.03271590173244476, + -0.09536059200763702, + 0.15819628536701202, + -0.0766533613204956, + -0.024421129375696182, + -0.05216841399669647, + 0.07733191549777985, + -0.018774371594190598, + -0.07642405480146408, + 0.041423968970775604, + 0.0980338305234909, + -0.034104153513908386, + 0.0809723287820816, + 0.03708045184612274, + 0.014617957174777985, + 0.028189968317747116, + 0.061048123985528946, + -0.03397853672504425, + -0.07693363726139069, + 0.0015141281764954329, + 0.07243525981903076, + 0.04445131868124008, + 0.06357616931200027, + -0.08368819952011108, + 0.01587417721748352, + -0.056925177574157715, + -0.029273537918925285, + -0.024318141862750053, + -0.0003657005727291107, + -0.008842531591653824, + -0.04723833501338959, + -0.0380922332406044, + 0.022867616266012192, + -0.07541817426681519, + 0.010260865092277527, + 0.03135620057582855, + 0.06604748219251633, + 0.10609836876392365, + -0.10410474240779877, + 0.08852698653936386, + 0.009995374828577042, + 0.013105723075568676, + 0.012148506939411163, + 0.06441330164670944, + 0.06649059057235718, + 0.36319077014923096, + 0.11472730338573456, + 0.15437336266040802, + -0.025218918919563293, + 0.09900757670402527, + -0.058816052973270416, + -0.04449968785047531, + -0.10797852277755737, + -0.10654433071613312, + -0.0006483225151896477, + 0.003746502334252, + 0.013195215724408627, + -0.032401517033576965, + -0.1107160747051239, + -0.02124457061290741, + 0.12844309210777283, + 0.033796221017837524, + -0.01959606260061264, + -0.268144428730011, + -0.25353094935417175, + -0.1021353229880333, + -0.038046207278966904, + -0.14028365910053253, + -0.06611426174640656, + -0.3484503924846649, + -0.001605837605893612, + -0.08129890263080597, + 0.011084948666393757, + 0.2756000757217407, + 0.08545108884572983, + -0.19038696587085724, + -0.013501301407814026, + 0.01933123543858528, + -0.04312758892774582, + 0.08983007073402405, + 0.07256431877613068, + 0.10655444115400314, + 0.03621809929609299, + 0.039520636200904846, + 0.09478918462991714, + 0.1374071091413498, + -0.034969158470630646, + -0.011547960340976715, + 0.03937073051929474, + -0.16031748056411743, + 0.06682133674621582, + 0.045192234218120575, + -0.021542273461818695, + -0.20430156588554382, + 0.07453259825706482, + -0.007856771349906921, + 0.035223569720983505, + -0.0031885793432593346, + 0.02184726484119892, + -0.03685733675956726, + 0.007050229236483574, + 0.03772386163473129, + -0.0434456467628479, + 0.03578038141131401, + 0.05124283954501152, + 0.014163815416395664, + 0.03499174118041992, + -0.09823087602853775, + 0.13536524772644043, + 0.07744167745113373, + 0.010919689200818539, + 0.016618533059954643, + -0.022410769015550613, + -0.1374606043100357, + -0.04627781733870506, + -0.12080005556344986, + -0.02588343434035778, + 0.01934906095266342, + 0.01329811755567789, + 0.03241441026329994, + 0.1076735332608223, + 0.023637855425477028, + -0.012423613108694553, + 0.03487344831228256, + 0.05695971101522446, + -0.01091839000582695, + -0.03192104771733284, + 0.01424544956535101, + -0.040929146111011505, + 0.025592224672436714, + -0.019848370924592018, + 0.013205738738179207, + -0.0736638754606247, + -0.019328203052282333, + 0.049452319741249084, + -0.012194344773888588, + -0.09528441727161407, + -0.047623373568058014, + -0.029851671308279037, + -0.02761661633849144, + 10.0, + 1.0, + 1.0, + 10.0, + 0.800000011920929, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + -0.2627194821834564, + -0.08338192105293274, + 0.10457402467727661, + -0.09953087568283081, + 0.10657195746898651, + -0.04237668961286545, + 0.04046979546546936, + -0.04557228088378906, + 0.07524226605892181, + 0.010811371728777885, + -0.015428174287080765, + -0.04506291449069977, + -0.00016407668590545654, + -0.00632842630147934, + -0.07258912175893784, + 0.08285130560398102, + -0.024627845734357834, + 0.04929331690073013, + -0.022250346839427948, + -0.009411709383130074, + 0.05935261771082878, + 0.018552571535110474, + -0.01787617802619934, + -0.0027199573814868927, + 0.007061678916215897, + -0.09229925274848938, + -0.050873104482889175, + -0.02214057371020317, + -0.06308695673942566, + -0.07688146829605103, + 0.0678730234503746, + -0.07070494443178177, + -0.11450380831956863, + 0.10990188270807266, + 0.01598985120654106, + -0.10175728797912598, + 0.04086305946111679, + 0.05252093821763992, + 0.07987197488546371, + 0.032755836844444275, + -0.015935435891151428, + -0.023860372602939606, + 0.034140896052122116, + -0.04016068950295448, + 0.02088184468448162, + 0.04457862675189972, + 0.03534994274377823, + -0.0559227354824543, + 0.04727504402399063, + 0.019021570682525635, + -0.013184720650315285, + 0.011693000793457031, + 0.0006452612578868866, + -0.02251325733959675, + 0.07004145532846451, + -0.03315579891204834, + -0.0244841817766428, + -0.09349390119314194, + -0.02726779505610466, + -0.015714101493358612, + -0.0039982870221138, + 0.06417158991098404, + -0.0786813348531723, + -0.0452067106962204, + 2.0, + 2.0999999046325684, + 0.20000000298023224, + 2.0, + 2.0, + 2.0, + 2.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + -0.2776360809803009, + -0.0027241557836532593, + -0.0386926531791687, + -0.16904371976852417, + 0.06612378358840942, + 0.11226408183574677, + -0.03934934735298157, + 0.017437433823943138, + 0.11316156387329102, + 0.02387884631752968, + -0.009991776198148727, + 0.03076770156621933, + 0.07005544751882553, + 0.11497834324836731, + -0.06531957536935806, + 0.03021989017724991, + 0.03970711678266525, + 0.06755318492650986, + -0.08334402740001678, + 0.11439217627048492, + 0.11479471623897552, + -0.0065034134313464165, + -0.05279252305626869, + 0.053659722208976746, + -0.014000765979290009, + -0.0148005411028862, + 0.09336672723293304, + -0.029114414006471634, + -0.07471160590648651, + -0.06404575705528259, + 0.1505538672208786, + -0.10338335484266281, + -0.11592800170183182, + 0.038822174072265625, + -0.0012075155973434448, + 0.010440908372402191, + -0.03062749281525612, + 0.004248034209012985, + 0.08898166567087173, + 0.02226579189300537, + -0.04741588234901428, + 0.044224563986063004, + 0.015376495197415352, + -0.06054031103849411, + 0.019038595259189606, + -0.013588536530733109, + 0.006732792593538761, + -0.02949635311961174, + 0.03741754591464996, + -0.0799962505698204, + 0.07693232595920563, + -0.03260757029056549, + 0.017619868740439415, + 0.08070360124111176, + -0.02404601126909256, + -0.0263291597366333, + -0.02193518541753292, + -0.03007059544324875, + 0.03987249732017517, + 0.025030575692653656, + -0.0212307907640934, + 0.05507592484354973, + -0.07318416237831116, + -0.09770633280277252, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5707110166549683, + 0.47957906126976013, + 0.904793918132782, + 0.6200000047683716, + 0.5483871102333069, + 0.48828125, + 1.0, + 0.5707110166549683, + 0.0, + 0.0, + 0.23978953063488007, + 0.47957906126976013, + 0.904793918132782, + 0.10000000149011612, + 0.20479999482631683, + 0.0, + 0.0, + 1.0 + ], + "feature_vector_hash": "sha256:ad6c3fa08c6faec4a1612a62803666278c86ef8a5b9065234195ac96dca7afdf", + "intent_probabilities": { + "new_task": 0.00018182395014460797, + "continue": 0.9997362663951666, + "redo": 8.190965468869704e-05 + }, + "tier_probabilities": { + "C0": 0.15477329876495408, + "C1": 0.271960396558861, + "C2": 0.5713167320181116, + "C3": 0.0019495726580731633 + } + }, + { + "name": "redo", + "input": { + "current_request": "不对,重写。你把边界条件漏掉了,必须覆盖空输入和负数。", + "history_user": [ + "写一个整数排序函数" + ], + "previous_answer": "```python\ndef sort_items(xs):\n return xs\n```", + "previous_outcome": "failure", + "route_history": [ + { + "tier": "C0", + "difficulty": 0.1, + "margin": 0.7 + } + ] + }, + "features": [ + -0.24179507791996002, + 0.0477469339966774, + 0.16371943056583405, + -0.03311523050069809, + 0.030537202954292297, + 0.17393344640731812, + -0.0891779363155365, + 0.21464811265468597, + 0.07985076308250427, + 0.02135319635272026, + 0.030984431505203247, + 0.0801464319229126, + 0.05310976877808571, + 0.05365227162837982, + -0.005258694291114807, + 0.020328829064965248, + -0.059848446398973465, + 0.05568014830350876, + 0.08228445053100586, + -0.012365385890007019, + -0.0011055134236812592, + -0.048688922077417374, + 0.036818988621234894, + -0.03874177485704422, + -0.013352125883102417, + 0.06222210079431534, + -0.04455557093024254, + -0.06359726935625076, + 0.03795578330755234, + -0.04649640619754791, + -0.07434192299842834, + -0.01825898513197899, + -0.0062035247683525085, + 0.09370072931051254, + 0.014853112399578094, + -0.05861387401819229, + 0.0006926180794835091, + 0.009987318888306618, + -0.019319787621498108, + 0.01943625509738922, + -0.09500198066234589, + -0.05296064168214798, + 0.04308351129293442, + -0.0047739427536726, + -0.037426598370075226, + 0.03655259683728218, + 0.007451512850821018, + 0.00728236511349678, + -0.015766102820634842, + 0.009149564430117607, + 0.02176787704229355, + -0.0773167684674263, + -0.02201441116631031, + 0.07144906371831894, + 0.06229078769683838, + 0.10948896408081055, + -0.027023043483495712, + -0.01734921708703041, + -0.01089155301451683, + -0.019251883029937744, + -0.040325310081243515, + -0.042293813079595566, + -0.005141511559486389, + 0.053855977952480316, + 0.012013889849185944, + 0.02229577675461769, + 0.0029546222649514675, + 0.026355883106589317, + 0.021849699318408966, + 0.10975782573223114, + 0.03211543709039688, + -0.028642162680625916, + 0.00395613070577383, + -0.008815916255116463, + 0.011127403937280178, + 0.021115340292453766, + 0.048602525144815445, + 0.059684790670871735, + 0.011515258811414242, + 0.009358007460832596, + 0.029799070209264755, + 0.01626439206302166, + 0.0465114451944828, + 0.024704409763216972, + -0.004978679586201906, + -0.0017500342801213264, + -0.00023166160099208355, + -0.052568141371011734, + -0.03772272914648056, + 0.028193017467856407, + 0.0007975950138643384, + -0.024977367371320724, + -0.01549552846699953, + 0.007653173059225082, + -0.012988281436264515, + -0.005162850022315979, + -0.0006510794628411531, + 0.016558025032281876, + 0.012491188943386078, + -0.014468200504779816, + 0.06106731295585632, + 0.019110456109046936, + 0.0531998910009861, + 0.025100955739617348, + -0.021446818485856056, + -0.030918648466467857, + -0.013898172415792942, + -0.007263105362653732, + -0.03190019726753235, + -0.017267879098653793, + -0.015884872525930405, + -0.06985988467931747, + -0.02117692120373249, + -0.03969120606780052, + 0.002665425417944789, + -0.005989908240735531, + -0.040659546852111816, + -0.030919600278139114, + -0.010557678528130054, + -0.005414300598204136, + 0.028045061975717545, + 0.007493454962968826, + -0.013210132718086243, + -0.050633177161216736, + -0.00395987369120121, + -0.004037255886942148, + -0.015353243798017502, + 0.015025129541754723, + -0.0049150073900818825, + -0.015906566753983498, + -0.009808661416172981, + -0.05790749937295914, + 0.050815753638744354, + 0.05532180145382881, + 0.02826804667711258, + -0.002993590198457241, + -0.009440737776458263, + -0.06745631992816925, + 0.0015126786893233657, + -0.020592154935002327, + 0.036246322095394135, + 0.022465988993644714, + 0.03777526319026947, + -0.0030139083974063396, + 0.0184720940887928, + -0.09807602316141129, + 0.03881070762872696, + -0.022981930524110794, + 0.0218515507876873, + -0.03246602043509483, + 0.013480821624398232, + -0.020063161849975586, + -0.15128757059574127, + -0.05124153569340706, + -0.007625739090144634, + 0.17344385385513306, + 0.10450201481580734, + 0.17323613166809082, + -0.056687988340854645, + 0.057103391736745834, + 0.06899730861186981, + -0.07524343580007553, + -0.01593906059861183, + -0.09366419911384583, + 27.0, + 1.0, + 1.0, + 27.0, + 0.8518518805503845, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + -0.14514757692813873, + 0.0016411840915679932, + 0.04168848693370819, + -0.0789916068315506, + -0.013229385018348694, + 0.1810377538204193, + 0.10232680290937424, + 0.060777805745601654, + -0.018583770841360092, + 0.21844716370105743, + -0.01192547008395195, + 0.15235020220279694, + -0.08858039975166321, + -0.10629570484161377, + -0.03808530047535896, + 0.05998672917485237, + 0.07742217183113098, + -0.010304071009159088, + -0.1270671933889389, + 0.07932211458683014, + -0.020454443991184235, + 0.15969276428222656, + -0.05063769966363907, + -0.02147083729505539, + 0.08764492720365524, + -0.08008293807506561, + -0.04681210592389107, + -0.018059145659208298, + -0.03625465929508209, + -0.04782616347074509, + 0.06227068603038788, + 0.022000666707754135, + 0.04785041883587837, + 0.051046065986156464, + -0.04192765802145004, + 0.040108419954776764, + 0.04718834161758423, + -0.03682307153940201, + 0.0023163259029388428, + -0.04169674590229988, + -0.06251704692840576, + -0.05882500857114792, + 0.02792060375213623, + -0.02688187174499035, + 0.018924009054899216, + 0.08226128667593002, + -0.03977669030427933, + 0.011075861752033234, + 0.021303869783878326, + 0.03655553236603737, + -0.04641391336917877, + -0.08201565593481064, + -0.07998925447463989, + 0.03900470584630966, + 0.050224803388118744, + 0.0005741193890571594, + -0.04100010544061661, + -0.03016224130988121, + 0.012678384780883789, + -0.01892581582069397, + 0.07841017842292786, + -0.13957273960113525, + 0.004886358976364136, + -0.00021061301231384277, + 0.0, + 0.10000000149011612, + 0.699999988079071, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.033098697662353516, + 0.04320593178272247, + 0.21849660575389862, + -0.019036203622817993, + 0.07338619232177734, + -0.005341801326721907, + 0.14474086463451385, + 0.024126701056957245, + 0.008696958422660828, + 0.16912388801574707, + -0.03435567021369934, + 0.165928915143013, + 0.034619156271219254, + -0.03663855418562889, + 0.05038297921419144, + 0.08915428072214127, + 0.1473352611064911, + -0.10173466056585312, + -0.02973582223057747, + 0.022181808948516846, + 0.059217896312475204, + 0.12595827877521515, + 0.04259432107210159, + -0.04086873680353165, + 0.07374806702136993, + -0.00025748834013938904, + -0.027563799172639847, + -0.017364762723445892, + 0.045351654291152954, + -0.11084794998168945, + 0.02416357211768627, + 0.10423672199249268, + -0.036246299743652344, + -0.02501874975860119, + -0.030268322676420212, + -0.01023704744875431, + 0.03169970586895943, + -0.019955383613705635, + 0.023066040128469467, + 0.08347053825855255, + -0.03973010927438736, + -0.061209194362163544, + 0.01015234924852848, + 0.005257952492684126, + 0.0212609451264143, + -0.06438419222831726, + 0.04697847738862038, + 0.04162757471203804, + 0.0332995280623436, + -0.04163410887122154, + -0.062771737575531, + 0.010815031826496124, + -0.008353250101208687, + 0.0027649900875985622, + -0.03540201112627983, + 0.0016427114605903625, + 0.00244077667593956, + 0.022086352109909058, + -0.02948169782757759, + 0.022650375962257385, + -0.02626679465174675, + -0.05315341427922249, + -0.006191408261656761, + -0.023918192833662033, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3481481373310089, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3332204520702362, + 0.0, + 0.0, + 0.05000000074505806, + 0.0010000000474974513, + 0.0, + 0.0, + 0.0 + ], + "feature_vector_hash": "sha256:5a76800083a78a426aedf02f8ad29d4093150df3b00c48e4e0ecb29540a003c4", + "intent_probabilities": { + "new_task": 0.0097030278838227, + "continue": 0.733213008869615, + "redo": 0.25708396324656235 + }, + "tier_probabilities": { + "C0": 0.5345023458459528, + "C1": 0.34486781008297107, + "C2": 0.11490032174784594, + "C3": 0.0057295223232301075 + } + }, + { + "name": "long_three_segments", + "input": { + "current_request": "分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。分析架构,比较事务边界和消息队列设计。 最后给出风险清单。", + "history_user": [ + "数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。数据库系统设计。", + "处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。处理幂等与一致性。", + "部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。部署和回滚。", + "验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。验证测试计划。" + ], + "previous_answer": "架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。架构设计第一步,确定边界。", + "context": { + "context_tokens_est": 23000 + }, + "tool_state": { + "pending_tool_calls": 1 + }, + "attachments": [ + { + "name": "design.md", + "media_type": "text/markdown" + } + ] + }, + "features": [ + -0.16654667258262634, + -0.060363203287124634, + -0.1474533975124359, + -0.14679889380931854, + -0.048472970724105835, + 0.1688016802072525, + 0.1462429016828537, + 0.04832622781395912, + -0.15325674414634705, + -0.036839209496974945, + 0.011155422776937485, + 0.051242683082818985, + 0.032582685351371765, + -0.11055423319339752, + -0.01592385768890381, + 0.006677150726318359, + -0.11873035877943039, + -0.014999940991401672, + 0.05204925313591957, + 0.14176464080810547, + -0.061475466936826706, + 0.12381617724895477, + -0.1391550451517105, + 0.01668594405055046, + 0.026984810829162598, + 0.00958230346441269, + 0.028665270656347275, + -0.05535396188497543, + 0.026338960975408554, + 0.017470020800828934, + 0.02858167700469494, + 0.008602198213338852, + -0.03222072869539261, + 0.03771749883890152, + 0.03541567176580429, + -0.04495038464665413, + -0.016754234209656715, + 0.001606132835149765, + -0.06975212693214417, + 0.05215085297822952, + -0.09537649154663086, + 0.09158279001712799, + 0.17013704776763916, + -0.041877906769514084, + 0.031139317899942398, + -0.09325674176216125, + -0.02452506311237812, + -0.01878688856959343, + -0.029716938734054565, + -0.000707977800630033, + 0.005424331873655319, + -0.06914301961660385, + 0.025061799213290215, + -0.03507326543331146, + -0.03992234170436859, + -0.012407466769218445, + -0.044137321412563324, + -0.06994421780109406, + -0.05678693577647209, + -0.10910805314779282, + -0.033877674490213394, + -0.056059423834085464, + -0.04824591428041458, + 0.0277184396982193, + 0.0019102427177131176, + 0.0025571677833795547, + 0.0016937077743932605, + 0.012911123223602772, + 0.011286262422800064, + 0.023603640496730804, + 0.009383564814925194, + 0.010394236072897911, + -0.0007685459568165243, + 0.0009760000975802541, + 0.0007460316992364824, + 0.0015723650576546788, + 0.015691878274083138, + 0.02200872078537941, + 0.0015399190597236156, + 0.0008276276057586074, + 0.010245291516184807, + 0.005582705605775118, + 0.0037255799397826195, + 0.004512131679803133, + 0.00907796062529087, + 0.001121886889450252, + -0.0001898583141155541, + -0.016638411208987236, + -0.008495336398482323, + -0.0006259699584916234, + -0.0012207962572574615, + -0.009588518179953098, + -0.007170343305915594, + -0.009392795152962208, + -0.005233511328697205, + -0.005415933672338724, + 0.002586748916655779, + 0.009753478690981865, + 0.005892477463930845, + -0.004242906346917152, + 0.007905971258878708, + 0.0023031928576529026, + 0.004634487442672253, + 0.0026836374308913946, + -0.0027193562127649784, + -0.004981008358299732, + -0.0020950662437826395, + -0.0031350646167993546, + -0.0062181358225643635, + -0.014589894562959671, + -0.006180888973176479, + -0.006769342813640833, + -0.0038534069899469614, + -0.01510937511920929, + 0.0020854496397078037, + 3.22852902172599e-05, + -0.011814734898507595, + -0.007442587520927191, + -0.004456678871065378, + -0.0009678071946837008, + 0.003089275909587741, + -0.0025371178053319454, + 0.0038612510543316603, + 0.00376225751824677, + -0.002284400397911668, + 0.000920242047868669, + -0.0010705420281738043, + 0.0012803698191419244, + -0.00012394538498483598, + 0.00012094724661437795, + -0.004197254311293364, + 0.0011568644549697638, + 0.008915143087506294, + -0.0003923656768165529, + 0.006405576132237911, + -0.0015540712047368288, + -0.0024259411729872227, + 0.0037262598052620888, + 0.001062151393853128, + 0.007352958898991346, + -0.007713085040450096, + -0.0028811029624193907, + -0.004607249516993761, + 0.004161208868026733, + -0.010914184153079987, + 0.005009357817471027, + -0.012223690748214722, + 0.00724641652777791, + -0.011799680069088936, + 0.0066442652605473995, + 0.02338569425046444, + -0.0062116580083966255, + 0.012409399263560772, + 0.0062492466531693935, + -0.014457601122558117, + 0.020281223580241203, + 0.003932061139494181, + 0.010075434111058712, + 0.012102504260838032, + 0.021407509222626686, + -0.0010557867353782058, + -0.012384065426886082, + 0.036361128091812134, + 0.04569236934185028, + 3050.0, + 2.0, + 1.0, + 3050.0, + 0.8944262266159058, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + -0.09529531002044678, + 0.05150163918733597, + -0.21269658207893372, + -0.18771015107631683, + 0.10029305517673492, + 0.006541337817907333, + 0.08701545000076294, + 0.15997427701950073, + -0.027915548533201218, + 0.019896874204277992, + 0.04722435399889946, + 0.08044388890266418, + -0.07976736128330231, + -0.12474792450666428, + -0.11262308061122894, + -0.09370949119329453, + 0.08719348907470703, + 0.02466461807489395, + 0.023397888988256454, + 0.029876217246055603, + 0.04393939673900604, + -0.05047030374407768, + -0.06623505055904388, + 0.054271530359983444, + 0.05616425722837448, + -0.10731735080480576, + 0.11529253423213959, + -0.008905787020921707, + 0.0528610423207283, + 0.0028935037553310394, + 0.051216550171375275, + -0.09227322041988373, + 0.026349250227212906, + 0.06261274963617325, + 0.007638223469257355, + 0.06547635793685913, + -0.09495401382446289, + 0.052670858800411224, + -0.0696505755186081, + 0.05342794209718704, + -0.08572301268577576, + 0.03021940216422081, + -0.02200911194086075, + -0.034898627549409866, + -0.005231063812971115, + -0.07957930862903595, + -0.014732507057487965, + 0.045695655047893524, + -0.019801169633865356, + 0.04990644007921219, + 0.16228201985359192, + 0.00270891934633255, + 0.12030938267707825, + 0.03221963346004486, + 0.08510779589414597, + 0.0008234903216362, + -0.005891744047403336, + -0.008337028324604034, + 0.1262533962726593, + 0.029055066406726837, + -0.031074542552232742, + 0.04663453251123428, + -0.09247267246246338, + -0.03996582329273224, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.13958561420440674, + -0.035805508494377136, + -0.1554982215166092, + -0.10746140778064728, + -0.0871262177824974, + 0.15006083250045776, + 0.02581055834889412, + 0.09986864030361176, + -0.08216449618339539, + 0.020923811942338943, + 0.09828901290893555, + 0.09582308679819107, + -0.014782119542360306, + -0.11615787446498871, + 0.009011179208755493, + 0.06393453478813171, + -0.04767560586333275, + 0.024854935705661774, + 0.1154467910528183, + 0.05768201872706413, + 0.033970609307289124, + 0.0044195642694830894, + -0.08610372990369797, + -0.00570007786154747, + -0.0114288330078125, + -0.020033616572618484, + -0.019398219883441925, + -0.12430033087730408, + -0.08945871889591217, + -0.05880895256996155, + -0.0032623857259750366, + 0.004060555249452591, + -0.011130739003419876, + 0.006958300247788429, + 0.0545634850859642, + -0.01861615851521492, + 0.042556144297122955, + 0.06049402058124542, + -0.03308968245983124, + 0.007616039365530014, + -0.15140655636787415, + -0.06874995678663254, + 0.054263144731521606, + -0.0035836149472743273, + 0.019249873235821724, + -0.04679572954773903, + 0.04520544409751892, + 0.016079526394605637, + 0.01744813472032547, + -0.05161536484956741, + 0.052854184061288834, + 0.022944290190935135, + 0.015187764540314674, + -0.01775684580206871, + -0.0010123234242200851, + 0.022093430161476135, + -0.11241573095321655, + 0.011693239212036133, + -0.02429082617163658, + -0.05013788864016533, + -0.017634909600019455, + 0.04062703624367714, + 0.022394588217139244, + 0.07130805402994156, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1704917997121811, + 0.8461538553237915, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.8023224472999573, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0 + ], + "feature_vector_hash": "sha256:c58ee8f5d4c4e30349b676dd10328e5abc0a6b6f85dc6ebddae4e62c951b2a65", + "intent_probabilities": { + "new_task": 0.10141748627905539, + "continue": 0.8680379083373503, + "redo": 0.030544605383594308 + }, + "tier_probabilities": { + "C0": 0.05402824910551404, + "C1": 0.2531453952084952, + "C2": 0.6893582334957867, + "C3": 0.003468122190203936 + } + }, + { + "name": "unicode_normalization", + "input": { + "current_request": " Café\r\nPlease compare A and B. ", + "history_user": [ + " 第一轮\r\n内容 " + ], + "previous_answer": " Résumé\r处理完成。 " + }, + "features": [ + 0.010013222694396973, + 0.3986490070819855, + 0.012495806440711021, + -0.014053160324692726, + 0.01120709627866745, + -0.07037272304296494, + 0.15158547461032867, + -0.07318547368049622, + -0.023428140208125114, + 0.022910621017217636, + -0.05241672694683075, + -0.09204056113958359, + 0.0928027331829071, + -0.01758081465959549, + -0.028937216848134995, + 0.05223599448800087, + -0.03486592695116997, + 0.009959101676940918, + -0.025034701451659203, + 0.017599690705537796, + -0.06025172397494316, + 0.027362652122974396, + -0.052478838711977005, + 0.0055196210741996765, + 0.06420072913169861, + -0.04052369296550751, + 0.03602106496691704, + -0.04448070749640465, + -0.047923583537340164, + 0.11402419954538345, + -0.01134247612208128, + 0.04637730494141579, + -0.09282390028238297, + -0.018928611651062965, + -0.08023464679718018, + 0.07044966518878937, + -0.03460944443941116, + -0.011109740473330021, + -0.05136522650718689, + 0.008425075560808182, + 0.04265962913632393, + -0.06524849683046341, + 0.11701204627752304, + 0.022814711555838585, + -0.023747459053993225, + -0.00958261638879776, + -0.029303602874279022, + 0.012779239565134048, + -0.04743701592087746, + -0.0102961715310812, + 0.08720140159130096, + 0.04242796078324318, + -0.010473756119608879, + 0.013783087953925133, + -0.0378388985991478, + -0.020812716335058212, + -0.025873012840747833, + -0.00594068318605423, + 0.03438371419906616, + -0.03138529509305954, + -0.0070827677845954895, + -0.05364145338535309, + 0.047400444746017456, + -0.010819941759109497, + 0.2417622208595276, + -0.12529422342777252, + -0.09063427895307541, + 0.004867502488195896, + -0.0490967258810997, + -0.006766938138753176, + 0.012078316882252693, + 0.017095837742090225, + -0.007411814760416746, + 0.0038525713607668877, + -0.03160592168569565, + 0.03651665523648262, + -0.007354736793786287, + 0.009072849527001381, + -0.05323198065161705, + -0.021785929799079895, + 0.03899890556931496, + -0.11368530243635178, + 0.04915979132056236, + -0.01991293393075466, + 0.019270580261945724, + -0.11106806248426437, + 0.15580196678638458, + -0.006391595117747784, + -0.013175521045923233, + -0.006388703361153603, + 0.053703904151916504, + -0.01160266064107418, + 0.11470607668161392, + -0.05612367391586304, + -0.1459515541791916, + 0.1545707732439041, + -0.02463093213737011, + 0.0573485866189003, + -0.08838316053152084, + 0.06878174841403961, + -0.028298145160079002, + 0.19256292283535004, + 0.0603247694671154, + -0.018983937799930573, + -0.1406986117362976, + 0.2515190541744232, + -0.08660697937011719, + 0.031847283244132996, + -0.07325682789087296, + 0.016040189191699028, + -0.004929874092340469, + 0.004239408299326897, + -0.05500434339046478, + -0.011826885864138603, + 0.10795003175735474, + -0.06015904247760773, + 0.04436413571238518, + 0.00594673166051507, + -0.061818432062864304, + 0.11262104660272598, + 0.07287082076072693, + -0.08362271636724472, + -0.09568464756011963, + 0.01771942712366581, + 0.09525910019874573, + -0.08734540641307831, + -0.11152888089418411, + -0.17527267336845398, + -0.03293686360120773, + 0.10446609556674957, + -0.024819515645503998, + 0.0064142365008592606, + -0.029445689171552658, + 0.06935557723045349, + -0.0012557627633213997, + -0.029628239572048187, + -0.013136029243469238, + -0.09660918265581131, + -0.0049924058839678764, + 0.03246428444981575, + -0.10553301870822906, + -0.11138194054365158, + 0.03754870966076851, + 0.03563622012734413, + -0.09049082547426224, + 0.015879523009061813, + 0.002041525673121214, + -0.03873920068144798, + 0.07570619136095047, + 0.005125241354107857, + 0.00803846213966608, + -0.033351946622133255, + -0.017004523426294327, + -0.013806347735226154, + -0.04041215777397156, + -0.03411192074418068, + 0.040169890969991684, + -0.05313161760568619, + 0.04750993475317955, + -0.012117008678615093, + 0.07587136328220367, + -0.061530083417892456, + 0.026165857911109924, + 0.02469797246158123, + 28.0, + 6.0, + 2.0, + 14.0, + 0.0, + 0.75, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + -0.2592179477214813, + 0.11520176380872726, + -0.018968114629387856, + 0.04874389246106148, + -0.050405390560626984, + 0.10098253935575485, + -0.02758537232875824, + -0.10798241198062897, + -0.14003489911556244, + 0.1087023913860321, + 0.022673284634947777, + -0.04660053551197052, + -0.11577315628528595, + -0.07726852595806122, + -0.010295100510120392, + 0.05065184831619263, + -0.1337933987379074, + 0.10018900781869888, + -0.020301897078752518, + -0.03846827894449234, + -0.09158788621425629, + 0.003672868013381958, + 0.014487594366073608, + -0.03255365788936615, + 0.08427012711763382, + -0.05680321156978607, + 0.0035808682441711426, + -0.03856336325407028, + 0.05395696684718132, + 0.016668863594532013, + 0.09320557117462158, + -0.08809749782085419, + 0.035985756665468216, + 0.012755020521581173, + 0.09833110868930817, + -0.03040429763495922, + -0.021927420049905777, + -0.03017205186188221, + 0.000543840229511261, + 0.14408227801322937, + -0.027322126552462578, + -0.014470450580120087, + -0.009073757566511631, + -0.023591434583067894, + -0.01962750032544136, + -0.06017853692173958, + 0.026626482605934143, + 0.07221710681915283, + -0.03167415410280228, + 0.061985231935977936, + -0.021646879613399506, + -0.015197213739156723, + 0.0030640345066785812, + 0.05591234564781189, + 0.0376773476600647, + 0.04695996642112732, + -0.03280392289161682, + 0.004586614668369293, + 0.04461352527141571, + -0.0035720542073249817, + 0.06799531728029251, + -0.005549015011638403, + -0.06654349714517593, + 0.037283532321453094, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.2731908857822418, + 0.1982807219028473, + 0.16408008337020874, + -0.13138023018836975, + 0.05835898220539093, + -0.0042578550055623055, + 0.05537401884794235, + -0.01985650509595871, + -0.0428883396089077, + 0.13072516024112701, + -0.10045701265335083, + 0.018187813460826874, + 0.03484538942575455, + -0.15396074950695038, + -0.03908565267920494, + 0.0722179263830185, + -0.09048104286193848, + 0.08814366906881332, + 0.131802499294281, + 0.06736212968826294, + 0.08546921610832214, + 0.10693150013685226, + -0.058467745780944824, + 0.033934980630874634, + -0.0627499595284462, + -0.05238858610391617, + 0.07149440050125122, + 0.016823425889015198, + -0.02080640196800232, + -0.006444741040468216, + 0.01313156820833683, + 0.02315092459321022, + -0.10826464742422104, + -0.025500653311610222, + 0.014206711202859879, + 0.027980510145425797, + -0.1205751821398735, + -0.10253920406103134, + -0.0857219398021698, + -0.01782134920358658, + 0.020235594362020493, + 0.033940017223358154, + -0.047537047415971756, + 0.0219738632440567, + -0.04428229480981827, + -0.022255785763263702, + 0.004709008149802685, + -0.055019576102495193, + 0.0927700623869896, + -0.016241032630205154, + 0.04552393779158592, + -0.06330838799476624, + 0.01071578823029995, + 0.0952216237783432, + -0.008081307634711266, + -0.03707844763994217, + -0.07543139159679413, + 0.05901043862104416, + 0.0009687915444374084, + -0.07925651967525482, + 0.02278769016265869, + 0.03602677956223488, + 0.010911626741290092, + -0.01805383712053299, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08571428805589676, + 0.3333333432674408, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.33672958612442017, + 0.0, + 0.0, + 0.0, + 0.0005499999970197678, + 0.0, + 0.0, + 0.0 + ], + "feature_vector_hash": "sha256:6cc86e8b03c51211d8e9e77c272b0664ba8b08f6473ed9f309cf2648287d9bfe", + "intent_probabilities": { + "new_task": 0.009013637388717851, + "continue": 0.9891145507721676, + "redo": 0.001871811839114563 + }, + "tier_probabilities": { + "C0": 0.3595849190292957, + "C1": 0.27204214469949384, + "C2": 0.3626737980632103, + "C3": 0.005699138208000368 + } + } + ] +} diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/tier/calibration.json b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/calibration.json new file mode 100644 index 0000000000..653ed1d05f --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/calibration.json @@ -0,0 +1 @@ +{"after_loss":0.7906094790154917,"before_loss":0.7925100512163812,"enabled":true,"temperature":1.090874386298676} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/tier/golden_vectors.json b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/golden_vectors.json new file mode 100644 index 0000000000..915e587e76 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/golden_vectors.json @@ -0,0 +1 @@ +{"calibration":{"after_loss":0.7906094790154917,"before_loss":0.7925100512163812,"enabled":true,"temperature":1.090874386298676},"cases":[{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.08610766083683977,0.4192342059039647,0.4930671940896457,0.001590939169549993],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.07493472670419632,0.4212757448725806,0.50282620012026,0.000963328302962936]},"features":[0.2271927297115326,0.001246139407157898,-0.10437075048685074,0.01772419363260269,0.01426348090171814,-3.739446401596069e-05,-0.03173252195119858,-0.08780141174793243,0.12102435529232025,-0.04758082330226898,-0.0014238283038139343,-0.06956596672534943,-0.043786417692899704,0.10375268757343292,0.05372927337884903,0.03535483032464981,-0.016197949647903442,-0.02530694752931595,0.023393090814352036,0.008109688758850098,0.05698921158909798,0.06294253468513489,0.04649005085229874,0.025195952504873276,-0.024891585111618042,0.04879217594861984,0.127475768327713,-0.040658071637153625,0.00022579357028007507,-0.0011517815291881561,-0.07425421476364136,-0.02780088782310486,-0.08883119374513626,-0.06902464479207993,-0.024615708738565445,-0.03350311517715454,0.021304909139871597,-0.05717994272708893,0.06704212725162506,-0.04403472691774368,-0.0399230495095253,-0.014289498329162598,0.03571464493870735,-0.011777466163039207,0.04950343444943428,0.022780271247029305,0.0007449798285961151,-0.0361713282763958,-0.009494569152593613,-0.014010053128004074,-0.04710948467254639,-0.014626255258917809,0.03404469043016434,-0.04297608882188797,0.00980566255748272,0.004397526383399963,0.028575431555509567,0.0405898317694664,0.01947510614991188,-0.024809394031763077,0.06188925355672836,-0.018722359091043472,-0.010530335828661919,-0.01185736432671547,0.5809037089347839,-0.23736044764518738,-0.10745149105787277,0.006178508047014475,-0.02191121317446232,0.007091960404068232,-0.04107978940010071,-0.011022726073861122,-0.031136581674218178,0.04225784167647362,-0.02634362317621708,0.008800599724054337,0.007005966734141111,0.013335313647985458,-0.07850735634565353,-0.013117359951138496,-0.008246433921158314,0.018294773995876312,0.019353579729795456,-0.012446426786482334,0.05200396105647087,-0.04504302516579628,-0.01444289367645979,-0.022888807579874992,0.027534203603863716,-0.0006576913874596357,0.022911500185728073,-0.02654639072716236,0.022586844861507416,-0.014555644243955612,-0.013579505495727062,0.11683805286884308,0.05537471920251846,-0.03189223259687424,-0.010020270012319088,-0.03381597250699997,-0.013695389032363892,-0.017883218824863434,0.030015062540769577,-0.02903672307729721,0.03856406733393669,0.07508080452680588,-0.013863369822502136,0.010825928300619125,-0.021285386756062508,0.0018973819678649306,0.03730952367186546,-0.011926699429750443,-0.04695291817188263,0.02320888824760914,0.014188681729137897,0.022535085678100586,0.01139330118894577,-0.013970562256872654,-0.047133151441812515,-0.009701546281576157,0.055175114423036575,0.006098239682614803,0.014667010866105556,-0.010524705983698368,-0.001014233916066587,0.02246156521141529,-0.05898670852184296,0.014115777797996998,0.028157755732536316,-0.013936103321611881,-0.037373993545770645,-0.00061549007659778,-0.02961242012679577,0.018494969233870506,0.015527116134762764,0.02074209600687027,0.08500168472528458,-0.04745620861649513,-0.0057913693599402905,0.00409818347543478,-0.011102968826889992,-0.06644615530967712,0.016051409766077995,0.0002879189560189843,-0.053008779883384705,0.016214828938245773,-0.005217639729380608,-0.040338627994060516,0.031159821897745132,0.020004453137516975,0.003747834824025631,-0.002765173790976405,0.0010477961041033268,0.007663321681320667,0.01311330683529377,0.014326324686408043,-0.020166615024209023,0.0027780558448284864,-0.003195313038304448,-0.030520016327500343,0.028931304812431335,0.02770138345658779,0.010546505451202393,-0.004132099915295839,667.0,107.0,9.0,74.11111450195312,0.0,0.7586206793785095,0.010494752787053585,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,3.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.8130841255187988,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.20690521597862244,0.07986576855182648,-0.13348063826560974,0.07952940464019775,0.13670378923416138,-0.06498891115188599,0.04904235154390335,-0.000582575798034668,0.015444375574588776,-0.04919424653053284,-0.003975216299295425,-0.038343414664268494,0.06148838251829147,0.03783619403839111,0.08819665759801865,0.13091467320919037,-0.010391121730208397,0.03408069163560867,0.02629983052611351,-0.03461834415793419,0.02897150255739689,0.015287630259990692,0.03880525380373001,0.06024354323744774,-0.05170189589262009,0.08152291178703308,0.046960532665252686,0.029616951942443848,0.012674437835812569,-0.0012077763676643372,-0.010901479050517082,-0.01688673347234726,-0.053351305425167084,-0.0004506576806306839,0.07768270373344421,0.009407341480255127,0.013062307611107826,-0.012906529009342194,0.07584255188703537,-0.0052324142307043076,0.0002085082232952118,-0.02466236799955368,-0.03067990019917488,-0.014758479781448841,0.0024167727679014206,-0.029470134526491165,-0.03971980884671211,0.04222283139824867,0.03772910684347153,-0.03085104562342167,-0.005788015201687813,0.006761245429515839,0.029275519773364067,0.018952321261167526,0.06055397540330887,-0.018416501581668854,-0.0597730427980423,0.011572964489459991,-0.009726237505674362,0.022801436483860016,0.05842212215065956,-0.02787916734814644,0.05861983448266983,0.022746212780475616,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25851574540138245,-0.005625709891319275,-0.14803990721702576,-0.011065270751714706,-0.040153101086616516,-0.02600717730820179,-0.10038043558597565,-0.12314744293689728,0.13950642943382263,-0.09533976018428802,-0.03284512460231781,-0.03396695852279663,-0.10570694506168365,0.07752451300621033,0.03476915508508682,-0.0004689851775765419,-0.019155379384756088,-0.028411738574504852,-0.00870051234960556,0.06731821596622467,0.05067578703165054,0.05769793689250946,0.010820083320140839,0.08109746873378754,-0.06373929232358932,0.007825173437595367,0.1170244812965393,-0.04806849732995033,-0.019257918000221252,-0.020753532648086548,0.036192089319229126,0.04225718602538109,-0.07102092355489731,0.029877817258238792,-0.06279623508453369,-0.08266288042068481,0.014803148806095123,-0.02166913077235222,0.059027452021837234,-0.019785135984420776,-0.005562368780374527,0.025854356586933136,0.04117029905319214,-0.005888170562684536,0.02993001788854599,-0.04815562441945076,0.004443892277777195,-0.01729757711291313,0.021647192537784576,-0.012897532433271408,0.0030643418431282043,0.006634123623371124,-0.002868982031941414,-0.03451944887638092,-0.025960253551602364,-0.02031160145998001,-0.04303538799285889,-0.0034492909908294678,0.022988591343164444,0.021399952471256256,0.03248748183250427,-0.007932446897029877,-0.007153713144361973,0.021771959960460663,1.0,0.0,0.0,0.0,0.0,1.0,0.6037870645523071,0.5010635256767273,0.0,0.3433283269405365,0.0,0.0,0.0,0.6037870645523071,0.0,0.0,0.650428831577301,0.5010635256767273,0.0,0.0,0.03099999949336052,0.0,0.0,0.0],"sample_id":"000252aa-f4e0-5e89-8d6f-aff9fd2d5206"},{"expected":{"calibrated_class":"C0","calibrated_probabilities":[0.6461417052995941,0.218710997158296,0.1331495455429772,0.001997751999132697],"uncalibrated_class":"C0","uncalibrated_probabilities":[0.6724510348593724,0.20627709581884904,0.12004216928496145,0.0012297000368170891]},"features":[-0.1874837875366211,0.2176925241947174,0.06428853422403336,0.13545964658260345,-0.1115870401263237,0.0099861528724432,0.07597401738166809,-0.10764665901660919,0.00798056647181511,0.03704189136624336,0.056351177394390106,-0.0047012511640787125,-0.0797542929649353,-0.08262322843074799,-0.021707363426685333,-0.006933874450623989,0.005838314071297646,-0.013192236423492432,-0.004748987033963203,-0.0344892218708992,0.026494061574339867,-0.025313949212431908,0.0021034181118011475,-0.07011718302965164,-0.028778642416000366,-0.12202045321464539,-0.03485279157757759,-0.05531344190239906,-0.015218351036310196,-0.06225254386663437,0.040932103991508484,0.033437829464673996,-0.041434600949287415,0.05650373920798302,0.10368061065673828,-0.015097599476575851,0.01957906410098076,0.0747511088848114,0.0473828949034214,-0.062329843640327454,0.06514695286750793,0.06173385679721832,0.006774061359465122,-0.050675518810749054,-0.01077330857515335,0.023026861250400543,0.058396853506565094,0.028509793803095818,-0.09342925250530243,-0.004606073256582022,0.010493021458387375,-0.07722865045070648,0.019380299374461174,0.11419067531824112,-0.037903208285570145,0.03052821010351181,0.070053830742836,-0.023908905684947968,-0.0064550600945949554,-0.02216017246246338,0.10233519971370697,0.055560484528541565,-0.00045209936797618866,-0.0002957955002784729,0.11455101519823074,-0.056232910603284836,0.22792470455169678,-0.006104484666138887,0.09314684569835663,0.04843873158097267,0.0013435783330351114,0.12666599452495575,0.06622447818517685,-0.6099086999893188,-0.36751922965049744,0.2606757879257202,-0.12699531018733978,0.006735207512974739,-0.04250539466738701,0.030610909685492516,-0.09311442077159882,0.043737150728702545,0.034807704389095306,-0.026976769790053368,-0.007944120094180107,-0.01189575158059597,0.006807413883507252,0.0034678520169109106,0.007284472230821848,0.008904725313186646,-0.07071011513471603,0.008374492637813091,0.0012507853098213673,-0.019026728346943855,-0.008588200435042381,0.007608428131788969,0.008587022311985493,-0.013321333564817905,0.041244905441999435,0.008774634450674057,0.00439375638961792,0.049406252801418304,-0.015705320984125137,-0.00575745664536953,-0.0011585857719182968,-0.014976742677390575,-0.018448997288942337,0.002091765170916915,0.020299576222896576,-0.02195868268609047,0.001667749136686325,-0.0231375340372324,0.03969189524650574,-0.02036229893565178,-0.03464166074991226,0.05138831213116646,0.02177385427057743,0.027486508712172508,-0.01051836647093296,-0.0368553102016449,-0.017400890588760376,-0.007928143255412579,0.013885268941521645,-0.00793690886348486,-0.035029541701078415,-0.007490832358598709,0.0004236078239046037,0.013714010827243328,-0.0048883091658353806,0.019454825669527054,0.012120245024561882,-0.0041168755851686,0.0006890093209221959,-0.013396501541137695,-0.002437783405184746,-0.0027153505943715572,0.009116011671721935,0.011589565314352512,0.004921626299619675,-0.013808726333081722,-0.023842064663767815,0.0011587608605623245,0.00026364639052189887,0.017284490168094635,0.024032341316342354,-0.02280246466398239,0.013259082101285458,0.018468793481588364,-0.0064735738560557365,-0.0026653907261788845,0.010859791189432144,-0.03332515433430672,0.0004055900499224663,-0.00474164355546236,-0.00794454850256443,-0.008148262277245522,0.00592180946841836,0.0025107853580266237,-0.009993497282266617,-0.013922176323831081,-0.004666192922741175,0.0002436682116240263,-0.003117332933470607,-0.007457343395799398,52.0,4.0,2.0,26.0,0.17307692766189575,0.3076923191547394,0.03846153989434242,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,-0.2862265110015869,0.17256686091423035,-0.1868399828672409,0.015174508094787598,-0.12454181909561157,0.00395769439637661,0.04224613308906555,-0.017343521118164062,0.03310355544090271,-0.08730609714984894,0.00030459463596343994,0.047994501888751984,0.07292042672634125,0.1024879515171051,-0.06193007528781891,-0.13482390344142914,0.028187241405248642,0.004325121641159058,-0.06313297152519226,-0.07993258535861969,-0.1246737688779831,0.03317397087812424,0.07209973782300949,0.014239337295293808,-0.11323697119951248,0.03723214566707611,0.047322891652584076,-0.0604277178645134,-0.06431597471237183,-0.02649221569299698,-0.006784040480852127,-0.04006977379322052,0.007135551422834396,-0.047497302293777466,0.010798942297697067,0.056955769658088684,0.028985608369112015,0.08208990097045898,-0.007202088832855225,-0.08791134506464005,-0.017683010548353195,0.011941760778427124,0.11721982806921005,-0.08398009091615677,-0.10318750143051147,0.008215878158807755,-0.03254402056336403,-0.03862113878130913,-0.021665411069989204,0.10764219611883163,0.10394182801246643,0.032839223742485046,0.03099760226905346,0.04276970028877258,-0.07122417539358139,-0.05207517743110657,0.041246529668569565,-0.05500771105289459,-0.08107751607894897,-0.08592216670513153,0.0350731760263443,-0.08083926141262054,0.00025872141122817993,0.033098720014095306,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-0.011472821235656738,0.13719293475151062,0.13046099245548248,0.03930112347006798,-0.07454492896795273,0.03285152465105057,0.0980183333158493,0.06790545582771301,-0.03534511849284172,0.08377839624881744,0.05611885339021683,-0.10800143331289291,0.10684721171855927,0.002453252673149109,0.010211490094661713,0.026865661144256592,-0.049917399883270264,-0.04614674672484398,-0.05911366641521454,0.012733085080981255,-0.07563598453998566,0.00832393579185009,-0.09579811990261078,-0.07032795250415802,0.03282683342695236,-0.05195179581642151,-0.005516473203897476,-0.052118100225925446,0.04483399540185928,-0.08768768608570099,0.04001373052597046,0.05201343819499016,0.02696644514799118,-0.01564711518585682,-0.026229102164506912,0.06770413368940353,-0.042394764721393585,-0.07484361529350281,-0.02739015221595764,0.1077592596411705,0.04224202409386635,0.05570201575756073,0.03548290953040123,0.004332289099693298,-0.059878453612327576,-0.08189834654331207,-0.010917811654508114,-0.045940425246953964,0.020323164761066437,0.01231573335826397,-0.016051407903432846,-0.15131132304668427,0.005112392827868462,0.06939328461885452,0.06258123368024826,-0.07234007120132446,-0.025966348126530647,-0.022198230028152466,-0.009217385202646255,0.009597547352313995,-0.04504867270588875,0.023675402626395226,0.05655419081449509,-0.014762938022613525,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.05000000074505806,0.0,0.0,0.0,0.0,0.0,0.0,0.39702919125556946,0.0,0.0,0.0,0.004050000105053186,0.0,0.0,0.0],"sample_id":"0002f865-997e-580a-9a6c-c93ff25b9e3c"},{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.18002164520397432,0.031582525817445486,0.7850261200235762,0.0033697089550039166],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.16265530837911926,0.02436129874495265,0.8108624484380672,0.0021209444378609207]},"features":[0.19540908932685852,-0.047253310680389404,-0.14079995453357697,-0.015178505331277847,0.10812124609947205,-0.06642849743366241,0.061161208897829056,-0.07694824039936066,-0.10192044079303741,-0.04116915911436081,-0.07972553372383118,-0.021203873679041862,-0.04534922167658806,0.08767305314540863,-0.02003081887960434,-0.011741810478270054,0.02953232079744339,0.08988482505083084,-0.0543738454580307,-0.0705256313085556,0.052022602409124374,0.0020773839205503464,0.00431244820356369,0.04831286519765854,0.0037041008472442627,-0.014593157917261124,0.0702851265668869,0.06293322145938873,0.02189391292631626,0.09603595733642578,0.00812646932899952,0.04647457227110863,0.07754935324192047,0.021456608548760414,0.018039830029010773,-0.017397407442331314,0.04396618902683258,-0.00898277759552002,0.0002991780638694763,0.0021367091685533524,0.008052382618188858,-0.009035177528858185,0.021388068795204163,-0.009386769495904446,-0.028928712010383606,-0.023818738758563995,0.04139406234025955,-0.017774265259504318,-0.0313352607190609,-0.032247450202703476,0.04742889478802681,0.027063466608524323,0.03636494278907776,-0.04081060737371445,-0.013807671144604683,-0.046351876109838486,0.01619913801550865,0.010842926800251007,-0.009679201990365982,0.003761284053325653,-0.023194488137960434,-0.0148774404078722,-0.05038382112979889,0.02326323837041855,0.6614606976509094,-0.22057583928108215,-0.03647168353199959,0.004030977375805378,0.11546221375465393,0.008453442715108395,-0.1589743196964264,0.013450387865304947,-0.01729252003133297,-0.02076835185289383,-0.024298878386616707,0.022518722340464592,-0.06554815173149109,0.043599728494882584,-0.12890391051769257,-0.09101846069097519,0.06824609637260437,-0.05367758125066757,-0.02207203209400177,0.05135158449411392,-0.031593918800354004,0.06477200239896774,-0.020217837765812874,-0.0036277163308113813,0.0165749192237854,0.00747483829036355,0.033838748931884766,0.07083086669445038,0.11590000987052917,0.01032948773354292,-0.007377207279205322,-0.00995755847543478,-0.10399526357650757,0.10286544263362885,-0.05456622689962387,-0.052865851670503616,-0.05242273211479187,0.04603726416826248,0.07715699076652527,-0.043604012578725815,-0.01443322654813528,-0.014743600971996784,0.010603356175124645,-0.0539909228682518,-0.061636969447135925,0.03704022616147995,-0.01924794167280197,0.06281458586454391,0.04747948423027992,-0.010786580853164196,0.06078368052840233,-0.10957151651382446,-0.011901341378688812,0.038693591952323914,0.11534373462200165,0.07508765161037445,0.09067130833864212,0.016845304518938065,-0.02132858708500862,0.014471281319856644,-0.05352618172764778,-0.03216048702597618,-0.08358399569988251,-0.01018597837537527,-0.0002448071027174592,-0.021786876022815704,-0.0021815351210534573,-0.02533668838441372,-0.04969622567296028,-0.009036943316459656,0.06588813662528992,0.015036833472549915,-0.01879161410033703,0.008270316757261753,-0.02195897325873375,0.005621667020022869,-0.0577344074845314,0.012832784093916416,0.010645818896591663,-0.0019234968349337578,-0.043603502213954926,0.017701048403978348,-0.007054146844893694,-0.054289381951093674,0.03335646912455559,0.02476549707353115,-0.019768649712204933,0.011911401525139809,-0.01632746309041977,0.05129676312208176,0.07840583473443985,0.011382411234080791,0.02098800241947174,0.008239767514169216,0.05375731736421585,-0.00249406136572361,0.00954885222017765,-0.02480972185730934,-0.00407508434727788,-0.05382638797163963,1787.0,247.0,39.0,45.82051467895508,0.0,0.7672076225280762,0.007274762261658907,0.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,10.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.023503078147768974,0.7206477522850037,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7488852739334106,0.0,0.0,0.0,0.022299999371170998,0.0,0.0,0.0],"sample_id":"000699fa-5cbb-5f4d-9e12-5992c1c9b25c"},{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.1046567307451252,0.27666514824138144,0.5816680344145992,0.03701008659889409],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.09341609665188272,0.2697584250370652,0.6067681981841964,0.030057280126855653]},"features":[0.11209720373153687,-0.12402911484241486,0.05929974839091301,-0.08892060816287994,-0.06548567116260529,0.1282137781381607,-0.06728805601596832,-0.0743999183177948,0.13771075010299683,0.025610849261283875,-0.05165467411279678,0.14387625455856323,0.04347740858793259,0.12157003581523895,0.03183617442846298,-0.0010119713842868805,0.032527051866054535,0.07443410903215408,-0.028669660910964012,-0.05103444680571556,0.03457760810852051,-0.08999118208885193,-0.02902219258248806,-0.07157240808010101,0.01598650962114334,-0.038567930459976196,0.00039770081639289856,-0.02083064243197441,0.03307506442070007,-0.05195268243551254,-0.045299045741558075,-0.07790645956993103,0.008810382336378098,-0.0851532518863678,-0.05523855239152908,-0.09891001880168915,0.07712686061859131,-0.04922657087445259,-0.087241530418396,0.02410402148962021,0.02384546771645546,0.0019202083349227905,0.025659849867224693,0.025600941851735115,0.01668241247534752,0.0017912425100803375,0.006999087519943714,0.03375096991658211,0.010273896157741547,0.03339289501309395,-0.016898291185498238,0.04855448380112648,-0.06171003729104996,0.020207280293107033,0.008497240021824837,0.005655646324157715,0.012172531336545944,-0.02699529379606247,-0.014359917491674423,0.03803511708974838,0.004665907472372055,0.01839682087302208,0.02081739343702793,0.04687214642763138,0.3616150915622711,-0.06518188863992691,-0.06088462471961975,0.008472451940178871,-0.021803531795740128,0.17779166996479034,-0.03320243954658508,-0.2866930663585663,1.3287528418004513e-05,-0.04797571897506714,0.016194572672247887,-0.011384721845388412,0.0029244162142276764,-0.03415309265255928,0.005168582312762737,-0.06493571400642395,-0.047311823815107346,-0.014187927357852459,0.0393703319132328,0.06974450498819351,0.07178333401679993,-0.023675573989748955,0.05400872975587845,0.028546515852212906,-0.040372420102357864,-0.01320929266512394,0.05163532868027687,0.06547750532627106,0.007037586998194456,-0.006095025688409805,-0.1221480667591095,0.03208451718091965,-0.006700484547764063,0.008978881873190403,-0.030016686767339706,-0.03353698551654816,-0.06825315952301025,-0.04786914214491844,-0.05254591256380081,0.008953824639320374,-0.08332259953022003,-0.07674261182546616,0.032826684415340424,0.0639144629240036,-0.005530219059437513,0.015915237367153168,-0.04671655595302582,0.015587035566568375,-0.008853457868099213,-0.014578169211745262,-0.03641270846128464,0.0025888755917549133,-0.012147142551839352,-0.033751364797353745,0.003661959897726774,-0.07297053933143616,0.036091189831495285,-0.09823641926050186,-0.07962331920862198,0.010563994757831097,-0.0041709174402058125,-0.004610935226082802,0.02334919385612011,-0.05922475829720497,-0.030164945870637894,-0.0476500429213047,0.07510323822498322,-0.018848005682229996,0.06003449484705925,-0.06430740654468536,-0.02290743589401245,0.03247528150677681,0.02638411521911621,0.002465088851749897,0.007417704910039902,0.012197320349514484,0.01501381117850542,-0.03725264221429825,-0.016987212002277374,-0.03282485902309418,-0.03783441334962845,-0.007836787961423397,-0.010010471567511559,-0.0007446326198987663,-0.0044931527227163315,-0.005748489871621132,-0.01149279996752739,0.006743249483406544,0.0335051566362381,-0.015794767066836357,-0.004014546517282724,-0.04179058223962784,0.011014490388333797,0.04197033494710922,0.026642365381121635,-0.00937097892165184,0.002681109355762601,0.007876682095229626,-3.8350000977516174e-05,-0.015294216573238373,217.0,33.0,4.0,54.25,0.04608295112848282,0.7327188849449158,0.0138248847797513,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9090909361839294,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5384495258331299,0.0,0.0,0.0,0.0027000000700354576,0.0,0.0,0.0],"sample_id":"00087820-69a2-5d81-bc6e-c241f0fe4df0"},{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.054535764649075114,0.2868976236044489,0.6388864827517741,0.01968012899470207],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.04525447006104377,0.27684175841805003,0.6630176295892933,0.014886141931612824]},"features":[-0.21443097293376923,-0.03253932297229767,0.006414398550987244,-0.12273433804512024,0.10528185963630676,0.038162559270858765,0.04085492715239525,0.04400382190942764,-0.06923475861549377,-0.1514551192522049,-0.04269780218601227,-0.10690807551145554,0.025273840874433517,-0.05786771699786186,0.02425777167081833,-0.21089959144592285,0.07756182551383972,0.038319431245326996,0.02584823966026306,0.024759288877248764,-0.022201795130968094,0.1325124055147171,-0.09122727811336517,-0.06828919798135757,-0.04743994027376175,-0.04582204669713974,-0.06645247340202332,-0.054839838296175,0.03437410295009613,0.0194135420024395,0.05220896750688553,-0.014453854411840439,-0.08763673156499863,-0.017129046842455864,-0.026574529707431793,-0.016001813113689423,0.024945814162492752,-0.07823220640420914,-0.05607149004936218,0.008936978876590729,0.014976035803556442,0.06495550274848938,-0.024200353771448135,0.028098778799176216,0.0149384755641222,0.06248562037944794,-0.0602782666683197,0.037504106760025024,0.027691885828971863,-0.08455681800842285,0.028594449162483215,-0.08716168999671936,-0.002255653962492943,0.03026914969086647,-0.08939752727746964,-0.026786919683218002,0.08703835308551788,-0.07891508936882019,-0.032081957906484604,-0.02035539597272873,0.021478280425071716,-0.05491426959633827,-0.016687948256731033,-0.045607633888721466,0.1879265159368515,-0.03630731627345085,-0.05260998755693436,0.01222106721252203,0.06113718822598457,0.032737743109464645,-0.0790974423289299,0.017041653394699097,-0.028669418767094612,0.027299392968416214,0.0366990864276886,0.051659781485795975,0.005016784183681011,0.028627794235944748,0.013902704231441021,-0.034271493554115295,0.004073935095220804,0.029203055426478386,0.036563508212566376,-0.0017863740213215351,0.004935266450047493,-0.016652371734380722,0.011673995293676853,0.03721578046679497,-0.028436310589313507,-0.05414505675435066,0.02604161947965622,0.04848705232143402,-0.026995178312063217,0.016735687851905823,-0.004498605616390705,0.023939257487654686,0.043548740446567535,0.05413489043712616,-0.010718585923314095,-0.00599986407905817,-0.05247047170996666,0.08531621098518372,0.008524174802005291,-0.01465198677033186,-0.01241111196577549,-0.032699886709451675,-0.037856657058000565,-0.01700231246650219,-0.016780773177742958,0.025603175163269043,-0.004906359128654003,0.046014729887247086,-0.02585734613239765,0.01671752892434597,0.010761376470327377,0.03991401568055153,-0.030733615159988403,-0.02459164708852768,0.006458860822021961,-0.019810743629932404,-0.021189115941524506,-0.07587561011314392,-0.0842178612947464,-0.004108154680579901,-0.029645761474967003,-0.01651395671069622,0.05348358675837517,-0.014553159475326538,0.005192369222640991,-0.004440378863364458,0.028301924467086792,-0.046893659979104996,0.01995844393968582,0.00554522592574358,0.0008118932601064444,-0.029559362679719925,0.07060229033231735,0.09091116487979889,-0.05000835657119751,-0.03746037930250168,-0.002559804590418935,0.03548024222254753,0.0078086890280246735,0.05882638692855835,-0.043559618294239044,0.02131158858537674,0.007510500028729439,-0.032617080956697464,-0.06425207108259201,0.03223419934511185,-0.014143096283078194,0.02276645228266716,0.03902644291520119,-0.0926167368888855,-0.05864541232585907,0.06842365115880966,-0.056694477796554565,0.005215019918978214,-0.05732349678874016,-0.07090888917446136,0.00497719831764698,0.010720727033913136,0.05744805559515953,-0.01973043382167816,139.0,10.0,1.0,139.0,0.2517985701560974,0.5899280309677124,0.014388489536941051,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.4892086386680603,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.14388489723205566,0.49416422843933105,0.0,0.0,0.0,0.0017000000225380063,0.0,0.0,0.0],"sample_id":"0009f8ab-24a8-5d24-bcf3-1f4c38c980ba"},{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.012648800712029349,0.054100060870950366,0.9239214889652497,0.009329649451770507],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.008735328496562956,0.04263662982865334,0.9423607042754397,0.0062673373993438865]},"features":[0.22351911664009094,-0.06489183008670807,0.11539721488952637,0.027557887136936188,0.04313138127326965,0.03544566035270691,-0.09007439017295837,0.04602908715605736,-0.023381242528557777,-0.06035175174474716,0.09471236169338226,-0.016682425513863564,-0.06948487460613251,-0.018239736557006836,0.06351057440042496,-0.0070722950622439384,-0.07994377613067627,0.0848364606499672,0.05249757692217827,-0.02921859547495842,-0.03882705792784691,-0.00973636656999588,-0.0987662822008133,-0.012674175202846527,-0.0366230234503746,0.016497492790222168,-0.06913010776042938,0.012026011943817139,0.0070295874029397964,-0.026451513171195984,-0.019596464931964874,-0.014785382896661758,0.017339857295155525,-0.015383703634142876,-0.0542544387280941,-0.057140108197927475,-0.048959508538246155,-0.027112003415822983,0.04252529516816139,-0.02076786756515503,0.013263050466775894,0.04666602239012718,0.01077244896441698,0.02583235315978527,0.00291619636118412,-0.016126390546560287,-0.00522217433899641,0.02768523246049881,-0.028627652674913406,0.028060002252459526,0.050150807946920395,0.03644201159477234,0.016796989366412163,0.03765019774436951,0.05639893561601639,-0.0739612951874733,0.022418197244405746,-0.0066039785742759705,-0.02451123669743538,0.01530570536851883,0.04869310185313225,-0.02777782455086708,0.04067277908325195,-0.004140473902225494,0.5369227528572083,-0.2043306976556778,0.05510263890028,-0.010604418814182281,-0.08766032010316849,-0.0209273062646389,0.05440448597073555,0.02112908475100994,-0.005515711847692728,0.02381301485002041,0.023236483335494995,-0.02708660252392292,0.026279041543602943,-0.00765078142285347,-0.0023283527698367834,0.07339730113744736,0.03427417203783989,-0.11621928960084915,0.002949499525129795,0.011231708340346813,-0.04588073119521141,-0.10669303685426712,-0.0944429412484169,0.002561239991337061,-0.03867844492197037,-0.014911435544490814,-0.055740877985954285,0.08283493667840958,-0.049476444721221924,-0.0039666155353188515,-0.054212577641010284,-0.06981246918439865,-0.055086880922317505,-0.05437793955206871,0.042533356696367264,-0.04428671672940254,0.05131808668375015,-0.04855937510728836,0.026860281825065613,-0.01180857140570879,-0.004623992834240198,0.020039185881614685,-0.02424144558608532,-0.029216058552265167,0.10458981245756149,-0.05033382028341293,-0.07451067119836807,-0.050739362835884094,0.0006428512278944254,0.1034996509552002,-0.05432857945561409,0.0789397656917572,-0.01547321304678917,-0.031000100076198578,0.018115133047103882,0.06510532647371292,-0.039998073130846024,0.022172836586833,0.0460551455616951,-0.020082714036107063,0.10459522157907486,-0.05206078290939331,-0.18109050393104553,0.040407150983810425,0.03595583140850067,-0.005941063165664673,0.05091599375009537,-0.03521044924855232,0.0036067813634872437,0.020734261721372604,0.038950879126787186,0.0008171928348019719,-0.07326509058475494,0.013924629427492619,-0.04031689092516899,0.018237126991152763,0.16919240355491638,-0.08194263279438019,-0.11573392897844315,0.004428189713507891,-0.10300866514444351,0.07136958092451096,0.08800923824310303,0.08664383739233017,0.057565052062273026,-0.054412778466939926,0.02079814299941063,-0.12150812894105911,-0.04150395840406418,0.10753680765628815,-0.004181038588285446,0.004343877080827951,0.02547955885529518,-0.044421564787626266,-0.08879668265581131,-0.039846353232860565,-0.04018222540616989,0.022254077717661858,-0.01272085215896368,-0.020157787948846817,332.0,50.0,2.0,166.0,0.0,0.8162650465965271,0.0060240961611270905,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.8799999952316284,1.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,1.0,0.23499754071235657,-0.26318681240081787,0.06405917555093765,-0.02991544082760811,0.053602904081344604,-0.13683877885341644,0.05032418668270111,0.0003335140645503998,0.019129730761051178,-0.02987843006849289,0.09102048724889755,0.05428265035152435,-0.04256462678313255,-0.1193566620349884,0.016283802688121796,0.014959382824599743,-0.020492933690547943,0.023225821554660797,-0.060250379145145416,-0.030512064695358276,0.00031853094696998596,-0.04599719122052193,0.0362083837389946,0.023527927696704865,0.006405867636203766,0.03571522980928421,0.013322710990905762,-0.019172348082065582,0.003703424707055092,0.018428128212690353,-0.027448154985904694,-0.009005293250083923,-0.0072441622614860535,0.021761344745755196,0.01440475508570671,-0.017553139477968216,-0.027310684323310852,-0.01739785261452198,-0.015169583261013031,-0.0036715976893901825,0.018193449825048447,-0.00810687243938446,-0.025057684630155563,-0.03965328261256218,0.03715769574046135,0.0328502431511879,-0.001557186245918274,0.029864078387618065,-0.08546066284179688,0.03523118048906326,-0.014618568122386932,0.027253098785877228,0.013523651286959648,-0.017163408920168877,-0.0018802210688591003,-0.024671953171491623,-0.02486938238143921,0.005807153880596161,-0.008122909814119339,0.02142464369535446,-0.005697567015886307,0.00589411286637187,0.01486135832965374,0.014298900961875916,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.054575115442276,0.14493490755558014,0.22252441942691803,0.0366363525390625,0.013203814625740051,-0.07705295830965042,0.10974062234163284,0.10018117725849152,-0.12079009413719177,0.07671321928501129,0.05269663780927658,-0.007306484505534172,0.0389615036547184,0.0075301602482795715,0.0629206970334053,-0.02704128623008728,0.008099481463432312,-0.018936730921268463,-0.08752991259098053,0.05290912464261055,0.1174381673336029,-0.08500537276268005,0.026767216622829437,-0.02194669097661972,-0.06184721738100052,-0.04749135300517082,-0.026500944048166275,-0.023106452077627182,0.05362864211201668,-0.07452715933322906,0.046276599168777466,-0.05881499499082565,0.07383692264556885,0.028420692309737206,-0.029844779521226883,0.06360189616680145,-0.02865973860025406,0.025582347065210342,0.0025666728615760803,0.01198771595954895,0.04269549623131752,-0.046107806265354156,-0.01083537470549345,-0.017131801694631577,-0.01725001260638237,-0.07334811985492706,0.012434176169335842,-0.027707185596227646,-0.00702204555273056,0.029455844312906265,-0.01539677008986473,-0.05655037984251976,0.027634521946310997,0.04302321374416351,0.008300958201289177,0.05402229726314545,-0.03803793340921402,8.655339479446411e-05,0.015047378838062286,0.04675915092229843,-0.019793514162302017,-0.006024087313562632,-0.0058452058583498,0.036484040319919586,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.022891566157341003,0.0,0.0,0.0,0.0,0.0,0.0,0.5808142423629761,0.0,0.0,0.0,0.07275000214576721,0.0,0.0,0.0],"sample_id":"000e6e41-2020-58c4-b480-eac14232b432"},{"expected":{"calibrated_class":"C3","calibrated_probabilities":[0.0031332663969484377,0.0664542748053726,0.20312946717151656,0.7272829916261625],"uncalibrated_class":"C3","uncalibrated_probabilities":[0.0019821525917083412,0.055489381822422267,0.18773994633804822,0.7547885192478211]},"features":[0.21808144450187683,-0.2721843719482422,0.02939518541097641,0.015401434153318405,-0.008072532713413239,0.08890365809202194,0.0645640566945076,-0.04243731498718262,0.07589137554168701,-0.03474161773920059,-0.0013090521097183228,0.01292361132800579,-0.014788445085287094,-0.061618708074092865,0.025500096380710602,0.014088903553783894,-0.07558949291706085,0.040443189442157745,0.028856683522462845,0.015576638281345367,0.020391665399074554,-0.054976917803287506,-0.003909103572368622,-0.03708405792713165,-0.030966974794864655,0.0157085582613945,-0.05835799500346184,-0.03611917421221733,0.013729259371757507,0.0243782177567482,-0.012306781485676765,-0.035145021975040436,0.005054604262113571,0.02189350686967373,-0.044951435178518295,0.00894663855433464,0.0142088383436203,0.041174691170454025,-0.02426759898662567,0.0010841898620128632,-0.002197224646806717,0.014001019299030304,0.012225937098264694,-0.013020024634897709,-0.05309818685054779,0.02065250650048256,0.021127503365278244,0.013461899012327194,-0.033107489347457886,-0.02498629502952099,0.015742775052785873,-0.0353286974132061,-0.025463851168751717,-0.0399814173579216,-0.013768773525953293,0.014501526951789856,0.009662721306085587,0.016641758382320404,0.002970818430185318,-0.007721163332462311,0.005595531314611435,-0.013466274365782738,-0.005216076970100403,0.010352537035942078,0.8258389830589294,-0.0468934029340744,-0.0956348106265068,-0.0029014761094003916,-0.10546477884054184,-0.009773705154657364,0.0026977122761309147,-0.01757740043103695,-0.01421690359711647,0.05314561352133751,-0.03432178124785423,-0.03864560276269913,-0.05099838599562645,0.05583870783448219,-0.035584885627031326,-0.005010142922401428,0.05012229084968567,-0.03266821801662445,0.01199042797088623,-0.05361505597829819,0.013965629041194916,-0.123587466776371,0.059596870094537735,-0.03798217326402664,0.035810038447380066,0.01772828958928585,-0.12787382304668427,0.005613275337964296,-0.043277494609355927,0.011379877105355263,0.014897175133228302,-0.010976539924740791,0.04007665812969208,-0.006996288895606995,-0.046992991119623184,-0.011574090458452702,-0.023952845484018326,-0.013609279878437519,-0.0007270291680470109,0.0020077989902347326,0.005408259574323893,-0.028687849640846252,0.005759724415838718,0.061790261417627335,0.039359692484140396,-0.01967764087021351,-0.0210240688174963,-0.015653343871235847,-0.0028742430731654167,-0.004767056088894606,-0.025472192093729973,-0.007554817479103804,0.002336355857551098,0.009325929917395115,-0.0014350248966366053,0.005732553079724312,0.03547586500644684,-0.008118067868053913,-0.01791366934776306,0.005168799310922623,0.0073282853700220585,0.045443687587976456,0.015647681429982185,-0.03560911491513252,0.007982132025063038,0.010282964445650578,-0.029294010251760483,-0.00857546366751194,0.017619630321860313,-0.017314855009317398,-0.007117739412933588,-0.010918209329247475,0.0001579807576490566,-0.007039918098598719,0.017875153571367264,-0.009437991306185722,0.03918181359767914,0.013192237354815006,-0.00299853109754622,0.07471917569637299,0.004945221357047558,-0.011531668715178967,0.0006934214034117758,-0.012892676517367363,-0.03469490632414818,0.005725327879190445,0.02412128448486328,0.009849675931036472,0.043862082064151764,-0.004912249743938446,-0.009472689591348171,-0.009888171218335629,0.01447307039052248,0.026254814118146896,0.01488555409014225,0.011308151297271252,0.017560111358761787,-0.015634685754776,0.0015723493415862322,0.02839643694460392,3142.0,444.0,47.0,66.85106658935547,0.01336728222668171,0.7536600828170776,0.018141310662031174,0.0,0.0,0.0,0.0,1.0,1.0,1.0,0.0,0.0,0.0,9.0,0.0,1.0,1.0,0.0,1.0,0.0,2.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.24188415706157684,0.6441441178321838,1.0,1.0,0.0,0.0,1.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.8052933216094971,0.0,0.0,0.0,0.039250001311302185,0.0,0.0,0.0],"sample_id":"00157511-ecff-5062-a586-d1fbed107814"},{"expected":{"calibrated_class":"C2","calibrated_probabilities":[0.0249802153949982,0.11043174949343457,0.7573643563964854,0.10722367871508186],"uncalibrated_class":"C2","uncalibrated_probabilities":[0.019120887651043996,0.09675313309020561,0.790434887266825,0.09369109199192545]},"features":[0.24310734868049622,-0.04663367569446564,-0.1858833134174347,-0.10586197674274445,0.04934287071228027,0.018895858898758888,0.03583654761314392,-0.20439012348651886,-0.0030672959983348846,-0.07119214534759521,-0.02947312593460083,0.012727614492177963,0.01672106236219406,0.02849254012107849,0.025550231337547302,-0.11705746501684189,-0.037166811525821686,0.02829887717962265,-0.028604554012417793,0.0231003500521183,0.0011629974469542503,-0.011856642551720142,-0.018639661371707916,0.0409594401717186,0.060693465173244476,-0.0415867455303669,-0.04097294434905052,-0.016916051506996155,-0.03045235201716423,0.0746525451540947,0.0023551788181066513,0.012807954102754593,0.019307276234030724,0.013549303635954857,-0.06632891297340393,-0.028585368767380714,0.03433746472001076,-0.08225928992033005,-0.06106603145599365,-0.041030045598745346,-0.052577003836631775,0.008030004799365997,-0.0455164834856987,0.020005280151963234,0.04211346060037613,0.01210465282201767,-0.060914043337106705,0.02869914472103119,0.043962135910987854,0.03934496268630028,-0.02352392114698887,-0.05185718089342117,0.04045245051383972,-0.008130257949233055,0.012940118089318275,-0.012415260076522827,0.007803451269865036,-0.020089518278837204,0.024246206507086754,-0.059939853847026825,-0.023223262280225754,0.01258285716176033,0.022272994741797447,-0.010492794215679169,0.5942202806472778,-0.16481992602348328,-0.07688772678375244,0.009827574715018272,0.1471148580312729,-0.001346866018138826,-0.10546639561653137,-0.005903811659663916,-0.05782558023929596,0.07193639874458313,0.010647517628967762,0.014144006185233593,-0.02386944741010666,0.06380999833345413,-0.10531231015920639,-0.007530965842306614,0.10519272834062576,0.043572794646024704,0.05473439395427704,-0.13515613973140717,0.1623646318912506,0.11528650671243668,0.06023078039288521,-0.0115051856264472,0.034074749797582626,0.004879140295088291,0.09197424352169037,0.07500423491001129,-0.013631564565002918,0.014041346497833729,-0.08167006820440292,-0.022909726947546005,-0.00559030007570982,-0.0009610811830498278,0.12929369509220123,0.11379904299974442,-0.09578534215688705,0.024052955210208893,0.022499313578009605,0.007511920295655727,0.033178526908159256,0.026634864509105682,-0.051389385014772415,0.03518655523657799,4.211245686747134e-05,0.02040061354637146,0.010915733873844147,0.005513591226190329,-0.014247178100049496,0.021732430905103683,-0.0339391753077507,0.008241997100412846,-0.011603694409132004,-0.00908383447676897,0.026336440816521645,0.020180195569992065,0.04726851359009743,-0.06210743263363838,-0.06545297801494598,0.011805534362792969,0.039806731045246124,-0.031753845512866974,0.006314877886325121,-0.017914259806275368,-0.002328308764845133,0.06731835752725601,-0.01585571840405464,-0.004164720885455608,-0.0083444993942976,0.03270021080970764,0.08004375547170639,-0.006695848889648914,-0.034686412662267685,0.018947791308164597,-0.020329231396317482,0.004126678220927715,-0.007034870330244303,-0.01331740990281105,0.021403266116976738,0.002808097517117858,0.028774604201316833,0.009428524412214756,-0.008709914050996304,-0.039631735533475876,0.009671402163803577,-0.004530327394604683,0.009634044952690601,-0.04645044356584549,0.019901826977729797,-0.02343001775443554,-0.04850699380040169,-0.005046341102570295,-0.029165495187044144,0.030565043911337852,-0.010608343407511711,-0.027341807261109352,0.009501760825514793,0.012458055280148983,-0.03495737537741661,-0.009416071698069572,1417.0,193.0,29.0,48.86206817626953,0.0,0.7685250639915466,0.03669724613428116,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,12.0,9.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.019054340198636055,0.7305699586868286,0.0,2.0,1.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,0.0,-1.0,0.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7257002592086792,0.0,0.0,0.0,0.01769999973475933,0.0,0.0,0.0],"sample_id":"00171d50-19ae-5f83-974f-a41d5108066a"}],"classes":["C0","C1","C2","C3"],"input_contract":{"features":"float32[batch,380]"},"output_contract":{"class_match":"exact","probability_atol":1e-06},"schema_version":"lightgbm_golden_vectors.v1","target":"tier","validation_policy_version":"lightgbm_golden_validation.v1"} \ No newline at end of file diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/tier/model.txt.gz b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/model.txt.gz new file mode 100644 index 0000000000..01b111a580 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/tier/model.txt.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60858e46ec2ddd090a5fa7a19cbb8011e71f944740f4b4033845c67764526b15 +size 12238618 diff --git a/src/opensquilla/squilla_router/lightgbm_model/assets/upstream-pyproject.toml b/src/opensquilla/squilla_router/lightgbm_model/assets/upstream-pyproject.toml new file mode 100644 index 0000000000..802c8161d3 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/assets/upstream-pyproject.toml @@ -0,0 +1,70 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "router-training-system" +version = "0.1.0" +description = "Versioned data, feature, training, evaluation, and model-registry pipeline for router classifiers" +readme = "README.md" +requires-python = ">=3.11" +license = "LicenseRef-Proprietary" +authors = [{name = "Router Training Team"}] +dependencies = [ + "pydantic>=2.7,<3", + "jsonschema>=4.22,<5", + "PyYAML>=6.0,<7", + "typer>=0.12,<1", + "numpy>=1.26,<3", + "pyarrow>=16,<24", + "scikit-learn>=1.5,<2", + "joblib>=1.4,<2", +] + +[project.optional-dependencies] +features = [ + "onnxruntime>=1.23,<2", + "tokenizers>=0.21,<1", +] +lightgbm = [ + "lightgbm>=4.6,<5", + "onnxruntime>=1.23,<2", + "tokenizers>=0.21,<1", +] +bert = [ + "torch>=2.3,<3", + "transformers>=4.55,<5", + "tokenizers>=0.21,<1", + "onnxruntime>=1.23,<2", + "onnx>=1.18,<2", +] +serving = [ + "lightgbm>=4.6,<5", + "onnxruntime>=1.23,<2", + "tokenizers>=0.21,<1", +] +dev = [ + "pytest>=8.2,<10", + "ruff>=0.12,<1", +] + +[project.scripts] +routerctl = "router_training.cli:app" +routermeasure = "router_training.serving_worker:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"router_training.resources" = ["*.json"] + +[tool.pytest.ini_options] +addopts = "-q" +testpaths = ["tests"] + +[tool.ruff] +target-version = "py311" +line-length = 100 + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] diff --git a/src/opensquilla/squilla_router/lightgbm_model/calibration.py b/src/opensquilla/squilla_router/lightgbm_model/calibration.py new file mode 100644 index 0000000000..b67a4a3001 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/calibration.py @@ -0,0 +1,41 @@ +"""Exact temperature calibration arithmetic extracted from training/common.py.""" +import math +from typing import cast + +import numpy as np + + +class TrainingError(ValueError): + """Invalid calibration inputs.""" + +def _as_logits(values: object) -> np.ndarray: + logits = np.asarray(values, dtype=np.float64) + if logits.ndim != 2 or logits.shape[0] == 0 or logits.shape[1] < 2: + raise TrainingError( + f"logits must have shape [rows, classes], got {logits.shape}" + ) + if not np.isfinite(logits).all(): + raise TrainingError("logits contain non-finite values") + return logits + +def _log_softmax(logits: np.ndarray) -> np.ndarray: + shifted = logits - np.max(logits, axis=1, keepdims=True) + return cast(np.ndarray, shifted - np.log(np.exp(shifted).sum(axis=1, keepdims=True))) + +def probabilities_to_logits(probabilities: object) -> np.ndarray: + values = np.asarray(probabilities, dtype=np.float64) + if values.ndim != 2 or values.shape[1] < 2: + raise TrainingError("probabilities must have shape [rows, classes]") + if not np.isfinite(values).all() or np.any(values < 0): + raise TrainingError("probabilities must be finite and non-negative") + row_sums = values.sum(axis=1, keepdims=True) + if np.any(row_sums <= 0): + raise TrainingError("each probability row must have positive mass") + normalized = values / row_sums + return cast(np.ndarray, np.log(np.clip(normalized, 1e-12, 1.0))) + +def apply_temperature(logits: object, temperature: float) -> np.ndarray: + matrix = _as_logits(logits) + if not math.isfinite(temperature) or temperature <= 0.0: + raise TrainingError("temperature must be finite and positive") + return cast(np.ndarray, np.exp(_log_softmax(matrix / temperature))) diff --git a/src/opensquilla/squilla_router/lightgbm_model/input_contract.py b/src/opensquilla/squilla_router/lightgbm_model/input_contract.py new file mode 100644 index 0000000000..3b2cf7f715 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/input_contract.py @@ -0,0 +1,21 @@ +"""Exact RouterInput contract extracted from the frozen training runtime.""" +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, JsonValue + + +class ContractModel(BaseModel): + model_config = ConfigDict(extra="forbid", frozen=True, use_enum_values=False) + +class RouterInput(ContractModel): + current_request: str + task_anchor: str = "" + history_user: tuple[str, ...] = Field(default=(), max_length=4) + previous_answer: str = "" + previous_usage: dict[str, JsonValue] = Field(default_factory=dict) + previous_outcome: Literal["success", "failure", "clarification", "unknown"] = "unknown" + active_route_tier: Literal["C0", "C1", "C2", "C3"] | None = None + route_history: tuple[dict[str, JsonValue], ...] = Field(default=(), max_length=5) + context: dict[str, JsonValue] = Field(default_factory=dict) + tool_state: dict[str, JsonValue] = Field(default_factory=dict) + attachments: tuple[dict[str, JsonValue], ...] = () diff --git a/src/opensquilla/squilla_router/lightgbm_model/model.py b/src/opensquilla/squilla_router/lightgbm_model/model.py new file mode 100644 index 0000000000..47b05f43c4 --- /dev/null +++ b/src/opensquilla/squilla_router/lightgbm_model/model.py @@ -0,0 +1,332 @@ +"""Offline loading of the original classifiers and frozen 380-feature runtime. + +Only the artifact registry adapter is replaced. Feature extraction and trained +transformers retain the frozen training implementation and original bytes. +""" + +from __future__ import annotations + +import gzip +import hashlib +import json +import threading +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +MODEL_SET_ID = "router-lightgbm-99d6ff75-d444135-a1" +MANIFEST_HASH = "sha256:29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357" +FEATURE_SCHEMA_VERSION = "lightgbm_380.v1" +INTENT_LABELS = ("new_task", "continue", "redo") +TIER_LABELS = ("C0", "C1", "C2", "C3") +_EXCLUDED_TYPES = frozenset({"run_manifest", "training_metrics"}) +# The packaged classifiers share the byte-identical legacy BGE assets. External +# bundles keep their self-contained short paths; both use the frozen digests. +_ENCODER_ASSETS = {"onnx_model": "model.onnx", "tokenizer": "tokenizer.json"} + + +class LightGBMModelError(RuntimeError): + """The configured classifier cannot safely execute.""" + + fail_closed = True + + +class LightGBMInferenceError(RuntimeError): + """A request failed; the model policy owns the source-compatible fallback.""" + + +def default_model_root() -> Path: + return Path(__file__).resolve().parent / "assets" + + +def _hash(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def _safe_path(root: Path, relative: str) -> Path: + path = (root / relative).resolve() + if path == root or root not in path.parents: + raise LightGBMModelError("bundled model path escapes its asset directory") + return path + + +def _verified_artifacts(model_root: str | Path | None) -> tuple[dict[str, Any], dict[str, Path]]: + root = ( + Path(model_root).expanduser().resolve() if model_root is not None else default_model_root() + ) + try: + manifest_bytes = (root / "model_manifest.json").read_bytes() + if _hash(manifest_bytes) != MANIFEST_HASH: + raise LightGBMModelError("bundled LightGBM model manifest hash mismatch") + manifest = json.loads(manifest_bytes) + if ( + manifest["model_set_id"] != MODEL_SET_ID + or manifest["input_schema_version"] != FEATURE_SCHEMA_VERSION + or manifest["outputs"] != {"intent": list(INTENT_LABELS), "tier": list(TIER_LABELS)} + ): + raise LightGBMModelError("bundled LightGBM model contract mismatch") + references = [ + ref + for ref in manifest["models"] + manifest["preprocess"] + if ref["artifact_type"] not in _EXCLUDED_TYPES + ] + paths = {} + for ref in references: + source = Path(ref["path"]) + if "/preprocess/" in ref["path"]: + local = "preprocess/" + ref["path"].split("/preprocess/", 1)[1].split("/", 1)[1] + elif "/bundles/" in ref["path"]: + local = "bundles/" + source.name + else: + local = source.parent.name + "/" + source.name + artifact_root = root + if local.startswith("preprocess/feature/artifacts/bge/"): + filename = _ENCODER_ASSETS[source.parent.parent.name] + local = f"preprocess/feature/artifacts/bge/{filename}" + if root == default_model_root(): + artifact_root = root.parents[1] / "models/v4.2_phase3_inference" + local = f"bge_onnx/{filename}" + compressed = ref["artifact_type"] == "lightgbm_model" + if compressed: + local += ".gz" + path = _safe_path(artifact_root, local) + if compressed: + with gzip.open(path, "rb") as handle: + payload = handle.read(ref["size"] + 1) + else: + payload = path.read_bytes() + if len(payload) != ref["size"] or _hash(payload) != ref["sha256"]: + raise LightGBMModelError(f"bundled LightGBM artifact hash mismatch: {local}") + paths[ref["path"]] = path + return manifest, paths + except LightGBMModelError: + raise + except (OSError, EOFError, ValueError, KeyError, TypeError) as exc: + raise LightGBMModelError("bundled LightGBM model assets are missing or invalid") from exc + + +def validate_artifacts(model_root: str | Path | None = None) -> dict[str, Any]: + """Verify the original manifest and every inference asset without ML imports.""" + manifest, paths = _verified_artifacts(model_root) + return { + "model_set_id": MODEL_SET_ID, + "model_manifest_hash": MANIFEST_HASH, + "input_schema_version": manifest["input_schema_version"], + "feature_vector_dim": 380, + "artifact_count": len(paths), + } + + +def _load_transformer(path: Path) -> Any: + """Remap the three frozen wrapper classes without installing router_training. + + The original joblib bytes refer to their training module. Hash verification + always precedes this call; all sklearn class resolution remains unchanged. + """ + from joblib.numpy_pickle import NumpyUnpickler + + from ._vendor.features import runtime + + wrappers = { + name: getattr(runtime, name) + for name in ( + "SklearnPCA64Projector", + "SklearnCharTfidfVectorizer", + "SklearnSVD100Projector", + ) + } + + class FrozenUnpickler(NumpyUnpickler): + def find_class(self, module: str, name: str) -> Any: + if module == "router_training.features.runtime" and name in wrappers: + return wrappers[name] + if module.startswith("router_training"): + raise LightGBMModelError("unexpected training class in frozen transformer") + return super().find_class(module, name) + + with path.open("rb") as handle: + return FrozenUnpickler(str(path), handle, ensure_native_byte_order=True).load() + + +@dataclass(frozen=True, slots=True) +class LightGBMPrediction: + intent_probabilities: dict[str, float] + tier_probabilities: dict[str, float] + intent_decision: str + tier_decision: str + feature_vector_hash: str + identity: dict[str, Any] + + +class LightGBMRouterModel: + """One local feature runtime shared by the intent and tier classifiers.""" + + source = "lightgbm_model" + _available = True + model_set_id = MODEL_SET_ID + manifest_hash = MANIFEST_HASH + feature_schema_version = FEATURE_SCHEMA_VERSION + feature_vector_dim = 380 + + def __init__(self, model_root: str | Path | None = None) -> None: + manifest, paths = _verified_artifacts(model_root) + self._lock = threading.RLock() + self._closed = False + self._encoder = None + try: + import joblib + import lightgbm as lgb + import numpy as np + import onnxruntime + import sklearn + import tokenizers + + from ._vendor.features.lightgbm380 import LightGBM380Featurizer + from ._vendor.features.runtime import BGEOnnxEncoder + + self._np = np + references = manifest["models"] + manifest["preprocess"] + + def one(kind: str, target: str = "") -> Path: + matches = [ + paths[ref["path"]] + for ref in references + if ref["artifact_type"] == kind and (not target or f"/{target}/" in ref["path"]) + ] + if len(matches) != 1: + raise LightGBMModelError(f"model has no unique {kind} artifact") + return matches[0] + + feature_root = one("transformer_manifest").parent + encoder_model = one("feature_artifact", "bge/onnx_model") + encoder = BGEOnnxEncoder( + encoder_model.parent, + model_path=encoder_model, + tokenizer_path=one("feature_artifact", "bge/tokenizer"), + ) + try: + self._featurizer = LightGBM380Featurizer( + embedder=encoder, + semantic_projector=_load_transformer(feature_root / "pca64.joblib"), + text_vectorizer=_load_transformer(feature_root / "tfidf_char_wb_2_4.joblib"), + character_projector=_load_transformer(feature_root / "svd100.joblib"), + ) + self._featurizer._fitted = True + except BaseException: + encoder.close() + raise + self._encoder = encoder + self._models = {} + self._temperatures = {} + for target in ("intent", "tier"): + model_file = one("lightgbm_model", target) + with gzip.open(model_file, "rt", encoding="utf-8") as handle: + text = handle.read() + self._models[target] = lgb.Booster(model_str=text) + calibration = json.loads( + one("temperature_calibration", target).read_text(encoding="utf-8") + ) + self._temperatures[target] = calibration["temperature"] + self.identity = { + "model_set_id": MODEL_SET_ID, + "model_manifest_hash": MANIFEST_HASH, + "input_schema_version": FEATURE_SCHEMA_VERSION, + "feature_vector_dim": 380, + "execution_mode": "native_embedded", + "model_type": "lightgbm", + "runtime_versions": { + name: module.__version__ + for name, module in { + "numpy": np, + "lightgbm": lgb, + "onnxruntime": onnxruntime, + "tokenizers": tokenizers, + "scikit-learn": sklearn, + "joblib": joblib, + }.items() + }, + } + except Exception as exc: + if self._encoder is not None: + self._encoder.close() + raise LightGBMModelError( + "cannot load bundled LightGBM runtime; install recommended dependencies" + ) from exc + + def _build_features(self, router_input: Mapping[str, Any]) -> Any: + from .input_contract import RouterInput + + if self._closed: + raise LightGBMInferenceError("LightGBM router model is closed") + canonical = RouterInput.model_validate(router_input).model_dump(mode="json") + features = self._featurizer.transform_row({"input": canonical}) + if features.shape != (380,) or not self._np.isfinite(features).all(): + raise LightGBMInferenceError("LightGBM feature contract was violated") + return features + + def build_features(self, router_input: Mapping[str, Any]) -> Any: + """Return the exact float32[380] feature vector for parity inspection.""" + with self._lock: + try: + return self._build_features(router_input).copy() + except (LightGBMModelError, LightGBMInferenceError): + raise + except Exception as exc: + raise LightGBMInferenceError("LightGBM feature extraction failed") from exc + + def predict_features(self, features: Any) -> LightGBMPrediction: + """Run both calibrated heads on a precomputed vector (golden tests).""" + from .calibration import apply_temperature, probabilities_to_logits + + with self._lock: + if self._closed: + raise LightGBMInferenceError("LightGBM router model is closed") + matrix = self._np.asarray(features, dtype=self._np.float32).reshape(1, -1) + if matrix.shape != (1, 380) or not self._np.isfinite(matrix).all(): + raise LightGBMInferenceError("LightGBM requires a finite float32[380] vector") + outputs = {} + for target, labels in (("intent", INTENT_LABELS), ("tier", TIER_LABELS)): + values = self._np.asarray( + self._models[target].predict(matrix, num_threads=1), dtype=self._np.float64 + ) + if ( + values.shape != (1, len(labels)) + or not self._np.isfinite(values).all() + or (values < 0).any() + ): + raise LightGBMInferenceError("LightGBM returned an invalid probability vector") + mass = values.sum(axis=1, keepdims=True) + if (mass <= 0).any(): + raise LightGBMInferenceError("LightGBM returned no probability mass") + calibrated = apply_temperature( + probabilities_to_logits(values / mass), + self._temperatures[target], + )[0] + outputs[target] = dict(zip(labels, map(float, calibrated), strict=True)) + return LightGBMPrediction( + intent_probabilities=outputs["intent"], + tier_probabilities=outputs["tier"], + intent_decision=max( + ("continue", "redo", "new_task"), key=outputs["intent"].__getitem__ + ), + tier_decision=max(TIER_LABELS, key=outputs["tier"].__getitem__), + feature_vector_hash=_hash(matrix.astype(" LightGBMPrediction: + with self._lock: + try: + return self.predict_features(self._build_features(router_input)) + except (LightGBMModelError, LightGBMInferenceError): + raise + except Exception as exc: + raise LightGBMInferenceError("LightGBM routing inference failed") from exc + + def close(self) -> None: + with self._lock: + if not self._closed: + assert self._encoder is not None + self._encoder.close() + self._closed = True diff --git a/src/opensquilla/squilla_router/models/v4.2_phase3_inference/artifact_manifest.json b/src/opensquilla/squilla_router/models/v4.2_phase3_inference/artifact_manifest.json index ff63b9d84f..d912898d6a 100644 --- a/src/opensquilla/squilla_router/models/v4.2_phase3_inference/artifact_manifest.json +++ b/src/opensquilla/squilla_router/models/v4.2_phase3_inference/artifact_manifest.json @@ -118,7 +118,7 @@ { "path": "router.runtime.yaml", "size_bytes": 5100, - "sha256": "ef2cbab9bc00ba295bf58d44f171f6f76b477a5a39149a4112cc7e74bbbccb5b", + "sha256": "0b7444cfcbf2fe922f613308a05bb49ff3c99cd32f40deb5aa22ecfd7b4a363d", "kind": "yaml_config", "source_note": "Router V4 Phase 3 bundle metadata or runtime configuration." }, diff --git a/src/opensquilla/squilla_router/models/v4.2_phase3_inference/router.runtime.yaml b/src/opensquilla/squilla_router/models/v4.2_phase3_inference/router.runtime.yaml index c3f3400570..5c901608b0 100644 --- a/src/opensquilla/squilla_router/models/v4.2_phase3_inference/router.runtime.yaml +++ b/src/opensquilla/squilla_router/models/v4.2_phase3_inference/router.runtime.yaml @@ -8,9 +8,9 @@ tier_mapping: tier_registry: S: [qwen/qwen3.7-flash] # Fast direct-answer tier for trivial turns and short acknowledgements. - M: [deepseek/deepseek-v4-flash-0731] # Default general tier for normal Q&A, editing, and bounded coding tasks. - L: [deepseek/deepseek-v4-pro-0813] # Reasoning tier for debugging, multi-step analysis, and structured planning. - XL: [z-ai/glm-5.3] # Highest GLM tier for architecture, high-risk work, and hard recovery turns. + M: [deepseek/deepseek-v4-flash] # Default general tier for normal Q&A, editing, and bounded coding tasks. + L: [deepseek/deepseek-v4-pro] # Reasoning tier for debugging, multi-step analysis, and structured planning. + XL: [moonshotai/kimi-k3] # Highest Kimi K3 tier for architecture, high-risk work, and hard recovery turns. tier_explanations: S: @@ -20,18 +20,18 @@ tier_explanations: expected_controls: "No explicit thinking; P0 prompt hint may shorten responses." M: route_class: R1 - model: deepseek/deepseek-v4-flash-0731 + model: deepseek/deepseek-v4-flash intent: "General-purpose default for routine product, coding, writing, and comparison tasks with bounded complexity." expected_controls: "Light or default thinking; P1 prompt policy unless the task is clearly trivial or high risk." L: route_class: R2 - model: deepseek/deepseek-v4-pro-0813 + model: deepseek/deepseek-v4-pro intent: "Reasoning tier for debugging, multi-signal diagnosis, multi-step implementation plans, and non-trivial technical analysis." expected_controls: "High thinking; P1/P2 prompt policy depending on flags and difficulty." XL: route_class: R3 - model: z-ai/glm-5.3 - intent: "Highest GLM tier for architecture design, cross-system tradeoffs, high-risk production decisions, and complex recovery turns." + model: moonshotai/kimi-k3 + intent: "Highest Kimi K3 tier for architecture design, cross-system tradeoffs, high-risk production decisions, and complex recovery turns." expected_controls: "High thinking; P1/P2 prompt policy depending on flags and difficulty." thresholds: diff --git a/tests/functional/test_gateway_attachment_history_e2e.py b/tests/functional/test_gateway_attachment_history_e2e.py index b3504b939d..d89b86ac8f 100644 --- a/tests/functional/test_gateway_attachment_history_e2e.py +++ b/tests/functional/test_gateway_attachment_history_e2e.py @@ -223,6 +223,8 @@ def _configure_gateway(tmp_path: Path) -> GatewayConfig: config.workspace_dir = str(tmp_path / "workspace") Path(config.workspace_dir).mkdir() config.attachments.media_root = str(tmp_path / "media") + # These fixtures exercise the legacy vision/sticky routing contract. + config.squilla_router.strategy = "legacy_v4_phase3" config.squilla_router.enabled = True config.squilla_router.rollout_phase = "full" config.squilla_router.require_router_runtime = False diff --git a/tests/functional/test_gateway_non_image_attachment_materialization_e2e.py b/tests/functional/test_gateway_non_image_attachment_materialization_e2e.py index 4a4d6a37f3..027a3d5850 100644 --- a/tests/functional/test_gateway_non_image_attachment_materialization_e2e.py +++ b/tests/functional/test_gateway_non_image_attachment_materialization_e2e.py @@ -232,6 +232,8 @@ def _configure_gateway(tmp_path: Path) -> GatewayConfig: config.state_dir = str(tmp_path / "state") config.workspace_dir = str(tmp_path / "workspace") config.attachments.media_root = str(tmp_path / "media") + # These fixtures exercise the legacy vision/sticky routing contract. + config.squilla_router.strategy = "legacy_v4_phase3" config.squilla_router.enabled = True config.squilla_router.rollout_phase = "full" config.squilla_router.require_router_runtime = False diff --git a/tests/test_application/test_turn_acceptance.py b/tests/test_application/test_turn_acceptance.py index a7e4d7f6aa..1450ba3225 100644 --- a/tests/test_application/test_turn_acceptance.py +++ b/tests/test_application/test_turn_acceptance.py @@ -195,3 +195,20 @@ async def test_pending_replay_validates_and_atomically_consumes_exact_revision() } ] assert ports.events.index("consume-pending") < ports.events.index("project") + + +@pytest.mark.parametrize("control", ["new_task", "redo"]) +async def test_router_controls_require_durable_acceptance(control) -> None: + ports = _ReplayPorts() + with pytest.raises(AdmissionUnavailableError, match="router controls"): + await DurableTurnAdmission(ports).admit( + replace(_command(), surface="webchat", router_control_event=control) + ) + + +async def test_internal_admission_validates_router_control() -> None: + ports = _ReplayPorts() + with pytest.raises(ValueError, match="routerControlEvent"): + await DurableTurnAdmission(ports).admit( + replace(_command(), surface="webchat", router_control_event="unknown") + ) diff --git a/tests/test_ci/test_plan_ci.py b/tests/test_ci/test_plan_ci.py index 13c97a5d1d..ff25f249e2 100644 --- a/tests/test_ci/test_plan_ci.py +++ b/tests/test_ci/test_plan_ci.py @@ -1280,6 +1280,19 @@ def test_darwin_recovery_test_keeps_macos_python_without_native_e2e( assert "macos_recovery_test_changed" in plan["reason_codes"] +@pytest.mark.parametrize("path", [ + "src/opensquilla/squilla_router/lightgbm_model/_vendor/features/runtime.py", + "src/opensquilla/squilla_router/lightgbm_model/assets/model_manifest.json", + "tests/test_lightgbm_router_model.py", +]) +def test_router_model_changes_keep_apple_silicon_parity( + tmp_path: Path, suite_config: dict[str, Any], path: str, +) -> None: + plan = _plan(tmp_path, suite_config, path) + assert "macos-recovery" in plan["required_suites"] + assert _platform_cells(plan, "macos-recovery") == {("macos-latest", "recovery")} + + def test_macos_recovery_test_routing_is_covered_by_suite_digest( suite_config: dict[str, Any], ) -> None: diff --git a/tests/test_ci/test_wait_ci_candidate.py b/tests/test_ci/test_wait_ci_candidate.py index bea914ccdb..7c3436f11e 100644 --- a/tests/test_ci/test_wait_ci_candidate.py +++ b/tests/test_ci/test_wait_ci_candidate.py @@ -51,6 +51,8 @@ def run(command, **kwargs): ("gh: Forbidden (HTTP 403); connection reset", 1, "http-403"), ("gh: Not Found (HTTP 404); i/o timeout", 1, "http-404"), ("gh: rate limit (HTTP 429)", 1, "http-429"), + ("gh: Bad Request (HTTP 400)", 1, "http-400"), + ("gh: Validation Failed (HTTP 422)", 1, "http-422"), ("net/http: TLS handshake timeout", 3, "tls-handshake-timeout"), ("read tcp: connection reset by peer", 3, "connection-reset"), ("wsarecv: An existing connection was forcibly closed by the remote host.", @@ -58,8 +60,9 @@ def run(command, **kwargs): ("dial tcp: i/o timeout", 3, "network-timeout"), ("net/http: request canceled (Client.Timeout exceeded while awaiting headers)", 3, "network-timeout"), - ("x509: certificate signed by unknown authority", 1, "unknown-cli-failure"), - ("unknown failure", 1, "unknown-cli-failure"), + ("x509: certificate signed by unknown authority", 1, "certificate-error"), + ("unknown failure", 3, "unknown-cli-failure"), + ("", 3, "unknown-cli-failure"), ]) def test_api_preserves_failure_and_bounds_retries(helper, monkeypatch, stderr, attempts, reason): calls, sleeps = [], [] @@ -109,9 +112,11 @@ def run(*args, **kwargs): raise failure monkeypatch.setattr(helper.subprocess, "run", run) + monkeypatch.setattr(helper.time, "sleep", lambda _: None) with pytest.raises(helper.MetadataApiError, match="unknown-cli-failure") as caught: helper.api("repos/owner/repo/actions/jobs/1") assert caught.value.__cause__ is failure + assert "attempts=3" in str(caught.value) assert "sensitive" not in str(caught.value) @@ -426,3 +431,40 @@ def sleep(seconds): assert len(calls) == 2 assert clock[0] == helper.WAIT_SECONDS assert not (tmp_path / "download").exists() + + +@pytest.mark.parametrize("returncode", [2, 4]) +def test_api_cli_cancellation_and_authentication_are_not_retried(helper, monkeypatch, returncode): + calls, sleeps = [], [] + failure = subprocess.CalledProcessError(returncode, ["gh"], stderr="TLS handshake timeout") + + def run(*args, **kwargs): + calls.append(args) + raise failure + + monkeypatch.setattr(helper.subprocess, "run", run) + monkeypatch.setattr(helper.time, "sleep", sleeps.append) + with pytest.raises(helper.MetadataApiError, match="attempts=1") as caught: + helper.api("repos/owner/repo/actions/jobs/1") + assert caught.value.__cause__ is failure + assert len(calls) == 1 + assert not sleeps + + +def test_api_unclassified_exit_one_can_recover_without_disclosing_cli_output(helper, monkeypatch): + calls, sleeps = [], [] + failure = subprocess.CalledProcessError( + 1, ["gh"], stderr="opaque-sensitive-cli-error", output="sensitive-body", + ) + + def run(command, **kwargs): + calls.append(command) + if len(calls) == 1: + raise failure + return subprocess.CompletedProcess(command, 0, stdout='{"status":"completed"}') + + monkeypatch.setattr(helper.subprocess, "run", run) + monkeypatch.setattr(helper.time, "sleep", sleeps.append) + assert helper.api("repos/owner/repo/actions/jobs/1") == {"status": "completed"} + assert len(calls) == 2 + assert sleeps == [1] diff --git a/tests/test_ci/test_windows_test_shards.py b/tests/test_ci/test_windows_test_shards.py index b2c6305303..8ad26a6a71 100644 --- a/tests/test_ci/test_windows_test_shards.py +++ b/tests/test_ci/test_windows_test_shards.py @@ -572,6 +572,10 @@ def test_runner_saturated_subprocess_contracts_are_marked_ci_serial() -> None: @pytest.mark.parametrize( ("test_file", "function_name"), [ + ( + "tests/test_cli/test_agent_invalid_api_key_process.py", + "test_invalid_api_key_exits_promptly_with_nonzero_status", + ), ( "tests/test_tools/test_shell_blocking_launch.py", "test_slow_native_launch_preserves_loop_and_ownership", diff --git a/tests/test_ci/test_workflows.py b/tests/test_ci/test_workflows.py index f3a2729f5b..d71b36a401 100644 --- a/tests/test_ci/test_workflows.py +++ b/tests/test_ci/test_workflows.py @@ -165,6 +165,7 @@ def test_ci_preparation_helpers_are_bound_by_the_existing_trust_policy() -> None policy = json.loads(Path(".github/ci/trust-policy.v1.json").read_text(encoding="utf-8")) assert { ".github/actions/download-required-artifact/action.yml", + ".github/actions/prepare-router-assets/action.yml", ".github/scripts/prepare-electron.mjs", } <= set(policy["merge_critical_inputs"]) @@ -3274,6 +3275,10 @@ def test_macos_recovery_runs_native_contracts_and_cannot_wash_failures_green() - } assert "if" not in setup_node assert job["steps"].index(setup_node) < job["steps"].index(test_step) + assert "tests/test_lightgbm_router_model.py" in test_step["run"] + preflight = next(step for step in job["steps"] + if step.get("name") == "Preflight offline test environment") + assert "platform.machine().lower() in {'arm64', 'aarch64'}" in preflight["run"] assert "tests/test_recovery" in test_step["run"] assert "tests/test_migration/test_opensquilla_home_migration.py" in test_step["run"] assert "tests/test_desktop/test_electron_startup_contract.py" in test_step["run"] @@ -3333,7 +3338,14 @@ def test_native_desktop_cells_run_source_and_frozen_mcp_probes() -> None: "Build production Docker image and verify MCP" )) assert container["if"] == "${{ matrix.probe == 'docker' }}" - assert 'git lfs pull --include="src/opensquilla/squilla_router/models/' in container["run"] + assets = next(step for step in steps if step.get("name") == ( + "Prepare router assets for Gateway probes" + )) + assert assets["if"] == "${{ matrix.probe != 'pty' }}" + assert assets["uses"] == "./.github/actions/prepare-router-assets" + for gateway_probe in (source, frozen, container): + assert steps.index(assets) < steps.index(gateway_probe) + assert "continue-on-error" not in assets assert "docker build --file Dockerfile" in container["run"] assert "docker run --rm -i --entrypoint python" in container["run"] assert "readonly" in container["run"] @@ -4095,3 +4107,85 @@ def test_offline_runner_uses_explicit_platform_ownership(job_name, platform): commands = [s["run"] for s in steps if "windows_test_shards.py run" in s.get("run", "")] assert len(commands) == 1 assert f"--platform {platform}" in commands[0] + + +@pytest.mark.parametrize("workflow,job_id,consumer", [ + ("ci.yml", "webui-chat-recovery", + "Run production-dist chat and Goal recovery browser contracts"), + ("ci.yml", "webui-capacity", "Run production-dist renderer capacity contracts"), + ("ci.yml", "ubuntu-quality", "Test targeted PR suite"), + ("ci.yml", "ubuntu-full", "Test Ubuntu full shard"), + ("ci.yml", "windows-full", "Test Windows shard"), + ("ci.yml", "windows-bundle", "Test bundle partition"), + ("ci.yml", "macos-recovery", "Test native profile recovery contracts"), + ("ci.yml", "desktop-recovery-e2e", + "Run cross-platform production-dist browser session hang contract"), + ("ci.yml", "skill-hub", "Run offline Community Skill contracts"), + ("ci.yml", "release-packaging", "Run release packaging contract tests"), + ("ci.yml", "main-canary", "Run offline gateway and provider canary"), + ("webui-browser-smoke.yml", "webui-browser-smoke", "Run Web UI browser smoke"), + ("live-release-e2e.yml", "live-release-e2e", "Run gateway LLM e2e"), + ("gateway-authorization-timing.yml", "timing", + "Compare offline Gateway startup and Default first turn"), +]) +def test_gateway_jobs_prepare_router_assets_before_execution(workflow, job_id, consumer): + steps = _workflow(workflow)["jobs"][job_id]["steps"] + assets = next(s for s in steps if s.get("uses") == "./.github/actions/prepare-router-assets") + execute = next(s for s in steps if s.get("name") == consumer) + python = next(s for s in steps if s.get("uses") == "actions/setup-python@v5") + checkout = next(s for s in steps if s.get("uses") == "actions/checkout@v4") + assert steps.index(checkout) < steps.index(python) < steps.index(assets) < steps.index(execute) + assert not assets.get("if") and not assets.get("continue-on-error") + + +def test_router_asset_action_verifies_assets_and_prepares_native_dependencies(): + action = yaml.safe_load( + Path(".github/actions/prepare-router-assets/action.yml").read_text(encoding="utf-8") + ) + assert action["runs"]["using"] == "composite" + step, native = action["runs"]["steps"] + assert native["if"] == "${{ runner.os == 'macOS' }}" + assert native["run"] == "brew install libomp" + assert step["shell"] == "bash" + assert step["working-directory"] == "${{ github.workspace }}" + assert "set -euo pipefail" in step["run"] + assert "src/opensquilla/squilla_router/models/**" in step["run"] + assert "src/opensquilla/squilla_router/lightgbm_model/assets/**" in step["run"] + assert step["run"].index("git lfs pull") < step["run"].index("validate_artifacts()") + assert "PYTHONPATH=src python" in step["run"] + assert "LightGBMRouterModel()" not in step["run"] + assert not step.get("continue-on-error") + + +@pytest.mark.parametrize("workflow,job_id", [ + ("docker-image.yml", "build-and-publish"), + ("wheelhouse-release.yml", "build-release-assets"), + ("wheelhouse-release.yml", "build-desktop-macos"), + ("wheelhouse-release.yml", "build-desktop-windows"), + ("windows-nsis-upgrade-regression.yml", "build"), +]) +def test_packagers_hydrate_and_validate_the_new_model(workflow, job_id): + steps = _workflow(workflow)["jobs"][job_id]["steps"] + hydrate = next(s for s in steps if "git lfs pull" in s.get("run", "")) + validate = next(s for s in steps if "validate_artifacts()" in s.get("run", "")) + assert "lightgbm_model/assets/**" in hydrate["run"] + assert steps.index(hydrate) <= steps.index(validate) + if workflow == "wheelhouse-release.yml": + # A release may check out a historical tag that predates the new bundle/action. + assert ("if [[ -f src/opensquilla/squilla_router/lightgbm_model/model.py ]]" + in validate["run"]) + assert not any(s.get("uses") == "./.github/actions/prepare-router-assets" for s in steps) + + +def test_gateway_timing_hydrates_both_independent_source_trees(): + for workflow, job_id in [ + ("ci.yml", "ubuntu-quality"), ("gateway-authorization-timing.yml", "timing"), + ]: + steps = _workflow(workflow)["jobs"][job_id]["steps"] + compare = next(s for s in steps if s.get("name") == + "Compare offline Gateway startup and Default first turn") + run = compare["run"] + assert "git archive" not in run + assert "lightgbm_model/assets/**" in run + assert "validate_artifacts()" in run + assert run.index("validate_artifacts()") < run.index("scripts/compare_gateway_timing.py") diff --git a/tests/test_cli/test_agent_invalid_api_key_process.py b/tests/test_cli/test_agent_invalid_api_key_process.py index 5a35d39585..7ee89e79c5 100644 --- a/tests/test_cli/test_agent_invalid_api_key_process.py +++ b/tests/test_cli/test_agent_invalid_api_key_process.py @@ -69,6 +69,8 @@ def _stop_process(process: subprocess.Popen[str]) -> tuple[str, str]: return process.communicate(timeout=5.0) +# Preserve the real five-second exit contract without competing pytest workers. +@pytest.mark.ci_serial def test_invalid_api_key_exits_promptly_with_nonzero_status(tmp_path: Path) -> None: response_sent = threading.Event() request_count = 0 @@ -111,18 +113,18 @@ def log_message(self, format: str, *args: object) -> None: assert env["OPENSQUILLA_TEST_PROFILE_LOCK_ROOT"] == "1" stack_dump_path = tmp_path / "agent-child-stacks.txt" stack_dump_path.touch() - # Record one bounded stack snapshot without filling the child's stderr pipe. - # Only include it in the test report if the process fails to make progress. + # Capture faults and the threads present when app() exits without an async + # frame walker racing code-object collection during normal imports. child_code = ( "import faulthandler\n" f"with open({str(stack_dump_path)!r}, 'w', encoding='utf-8') as stack_dump:\n" - " faulthandler.dump_traceback_later(\n" - f" {_REQUEST_START_TIMEOUT_SECONDS / 2!r}, file=stack_dump)\n" + " faulthandler.enable(file=stack_dump)\n" " try:\n" " from opensquilla.cli.main import app\n" " app()\n" " finally:\n" - " faulthandler.cancel_dump_traceback_later()\n" + " faulthandler.dump_traceback(file=stack_dump)\n" + " faulthandler.disable()\n" ) process = subprocess.Popen( [ @@ -143,18 +145,22 @@ def log_message(self, format: str, *args: object) -> None: try: if not response_sent.wait(timeout=_REQUEST_START_TIMEOUT_SECONDS): + returncode_at_timeout = process.poll() stdout, stderr = _stop_process(process) pytest.fail( "agent CLI did not reach the local provider within the startup limit\n" + f"return code before cleanup: {returncode_at_timeout!r}\n" f"stdout:\n{stdout}\nstderr:\n{stderr}\n" f"child stacks:\n{stack_dump_path.read_text(encoding='utf-8', errors='replace')}" ) try: stdout, stderr = process.communicate(timeout=_POST_RESPONSE_EXIT_TIMEOUT_SECONDS) except subprocess.TimeoutExpired: + returncode_at_timeout = process.poll() stdout, stderr = _stop_process(process) pytest.fail( - "agent CLI remained alive after the provider returned HTTP 401\n" + "agent CLI did not finish after the provider returned HTTP 401\n" + f"return code before cleanup: {returncode_at_timeout!r}\n" f"stdout:\n{stdout}\nstderr:\n{stderr}\n" f"child stacks:\n{stack_dump_path.read_text(encoding='utf-8', errors='replace')}" ) diff --git a/tests/test_engine/goldens/model_tier_policy_reference.json b/tests/test_engine/goldens/model_tier_policy_reference.json new file mode 100644 index 0000000000..dfdb461055 --- /dev/null +++ b/tests/test_engine/goldens/model_tier_policy_reference.json @@ -0,0 +1,3304 @@ +[ + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c0", + "c0", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c0", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c3", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c3", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c3", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": true, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": true, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": true, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": true, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": true, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": true, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": true, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": true, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c1", + "c1", + "T1", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c1", + "reason": "classifier_selected", + "prediction": "c1", + "probabilities": { + "c0": 0.0, + "c1": 1.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": true, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": true, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T2", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": true, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": true, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c2", + "reason": "classifier_selected", + "prediction": "c2", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 1.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": true, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": true, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": true, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": true, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c3", + "c3", + "T3", + "P2", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c3", + "reason": "classifier_selected", + "prediction": "c3", + "probabilities": { + "c0": 0.0, + "c1": 0.0, + "c2": 0.0, + "c3": 1.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "continue", + "c3", + "c0", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c0", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P2", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "redo", + "c3", + "c1", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.0, + "continue": 0.0, + "redo": 1.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c3", + "c0", + "T3", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P2", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + true, + { + "source": "rule", + "run_status": "not_run", + "final": "new_task", + "reason": "explicit_new_task_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "current-input", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + true, + { + "source": "rule", + "run_status": "not_run", + "final": "new_task", + "reason": "explicit_new_task_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "current-input", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + true, + { + "source": "rule", + "run_status": "not_run", + "final": "new_task", + "reason": "explicit_new_task_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "current-input", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + true, + { + "source": "rule", + "run_status": "not_run", + "final": "new_task", + "reason": "explicit_new_task_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + "current-input", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "redo_without_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "rule", + "run_status": "not_run", + "final": "redo", + "reason": "explicit_redo_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T0", + "P0", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "redo_without_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 1.0, + "c1": 0.0, + "c2": 0.0, + "c3": 0.0 + }, + "confidence": 1.0 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "rule", + "run_status": "not_run", + "final": "redo", + "reason": "explicit_redo_control", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 0.3333334, + "continue": 0.3333333, + "redo": 0.3333333 + }, + "confidence": 0.3333334 + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 0.26, + "c1": 0.25, + "c2": 0.25, + "c3": 0.24 + }, + "confidence": 0.26 + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.5, + "continue": 0.5, + "redo": 0.0 + }, + "confidence": 0.5 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "redo", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "redo", + "reason": "classifier_selected", + "prediction": "redo", + "probabilities": { + "new_task": 0.5, + "continue": 0.0, + "redo": 0.5 + }, + "confidence": 0.5 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_redo_upgrades_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c0", + "c0", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "classifier", + "run_status": "ran", + "final": "c0", + "reason": "classifier_selected", + "prediction": "c0", + "probabilities": { + "c0": 0.25, + "c1": 0.25, + "c2": 0.25, + "c3": 0.25 + }, + "confidence": 0.25 + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "error", + "final": "continue", + "reason": "classifier_error:RuntimeError", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "not_run", + "final": "new_task", + "reason": "no_active_task", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "fallback", + "run_status": "error", + "final": "c1", + "reason": "classifier_error:RuntimeError", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 1, + null, + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "fallback", + "run_status": "error", + "final": "continue", + "reason": "invalid_classifier_result", + "prediction": null, + "probabilities": null, + "confidence": null + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "new_task", + "c2", + "c2", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "new_task", + "reason": "classifier_selected", + "prediction": "new_task", + "probabilities": { + "new_task": 1.0, + "continue": 0.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "fallback", + "run_status": "error", + "final": "c2", + "reason": "invalid_classifier_result", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 1, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c1", + "c1", + "T2", + "P1", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c1", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": false + } + ], + [ + "continue", + "c2", + "c2", + "T3", + "P2", + false, + { + "source": "classifier", + "run_status": "ran", + "final": "continue", + "reason": "classifier_selected", + "prediction": "continue", + "probabilities": { + "new_task": 0.0, + "continue": 1.0, + "redo": 0.0 + }, + "confidence": 1.0 + }, + { + "source": "not_run", + "run_status": "not_run", + "final": "c2", + "reason": "model_continue_keeps_previous_tier", + "prediction": null, + "probabilities": null, + "confidence": null + }, + 3, + "generation-start", + { + "high_risk": false, + "debug": false, + "repo_arch": false, + "strict_format": false, + "long_context": true + } + ] +] diff --git a/tests/test_engine/test_capability_gate.py b/tests/test_engine/test_capability_gate.py index 850588daa7..f7b013219c 100644 --- a/tests/test_engine/test_capability_gate.py +++ b/tests/test_engine/test_capability_gate.py @@ -508,7 +508,8 @@ def _run_step( def _gate_config() -> GatewayConfig: - config = GatewayConfig() + # Walk-up belongs to the explicit legacy rule policy. + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.llm.provider = "mainprov" config.llm.model = "dummy-base-model" config.squilla_router.enabled = True diff --git a/tests/test_engine/test_heuristic_router_strategy.py b/tests/test_engine/test_heuristic_router_strategy.py index 708c3b35ec..22e3d94e3e 100644 --- a/tests/test_engine/test_heuristic_router_strategy.py +++ b/tests/test_engine/test_heuristic_router_strategy.py @@ -94,7 +94,7 @@ def make_context( session_key: str = "agent:heuristic:test", attachments: list[dict] | None = None, ) -> TurnContext: - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.squilla_router.rollout_phase = "full" return TurnContext( message=message, @@ -472,7 +472,7 @@ def test_status_reports_loaded_v4_runtime(monkeypatch: pytest.MonkeyPatch) -> No import opensquilla.squilla_router.v4_phase3 as v4_phase3 monkeypatch.setattr(v4_phase3, "V4Phase3Strategy", LoadedFakeV4Strategy) - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) squilla_router_step.preload_strategy(config.squilla_router) status = router_runtime_status() diff --git a/tests/test_engine/test_historical_image_followup.py b/tests/test_engine/test_historical_image_followup.py index c5e2604317..a7a65e115a 100644 --- a/tests/test_engine/test_historical_image_followup.py +++ b/tests/test_engine/test_historical_image_followup.py @@ -3,7 +3,7 @@ import base64 import json from collections.abc import AsyncIterator -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path from types import SimpleNamespace from typing import Any @@ -47,6 +47,7 @@ class _TranscriptEntry: reasoning_content: str | None = None token_count: int | None = None assistant_replay: dict[str, Any] | None = None + session_id: str = "" @dataclass @@ -59,6 +60,7 @@ class _FakeSessionManager: def __init__(self) -> None: self._nodes: dict[str, _SessionNode] = {} self._transcripts: dict[str, list[_TranscriptEntry]] = {} + self.storage = self async def create(self, session_key: str) -> _SessionNode: node = _SessionNode(session_key=session_key, session_id=f"id-{len(self._nodes) + 1}") @@ -77,9 +79,31 @@ async def append_message( self._transcripts.setdefault(session_key, []).append(entry) return entry - async def get_transcript(self, session_key: str) -> list[_TranscriptEntry]: + async def get_transcript( + self, session_key: str, *, expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> list[_TranscriptEntry]: + node = self._nodes[session_key] + assert expected_session_id in (None, node.session_id) + assert expected_session_epoch in (None, 0) return list(self._transcripts.get(session_key, [])) + get_canonical_transcript = get_transcript + + async def get_model_router_transcript( + self, session_key: str, *, expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> list[_TranscriptEntry]: + node = self._nodes[session_key] + assert expected_session_id in (None, node.session_id) + assert expected_session_epoch in (None, 0) + return [replace(row, session_id=node.session_id) + for row in await self.get_canonical_transcript(session_key)] + + async def get_canonical_transcript_coverage(self, session_id: str) -> SimpleNamespace: + assert any(node.session_id == session_id for node in self._nodes.values()) + return SimpleNamespace(canonical_complete=True) + async def get_session(self, session_key: str) -> _SessionNode | None: return self._nodes.get(session_key) @@ -943,7 +967,17 @@ async def test_current_image_and_one_archived_image_id_are_merged_and_exact() -> @pytest.mark.asyncio -async def test_persisted_compacted_archive_rehydrates_explicit_attachment_id() -> None: +async def test_persisted_compacted_archive_rehydrates_explicit_attachment_id(monkeypatch) -> None: + from opensquilla.engine.steps import squilla_router + from opensquilla.provider.selector import ModelSelector, ProviderConfig, SelectorConfig + + # Exercise current model-policy admission on a proven vision deployment; + # this test owns archived-image hydration, not learned tier selection. + classifier = SimpleNamespace(predict=lambda _input: { + "intent_probabilities": {"new_task": 1.0, "continue": 0.0, "redo": 0.0}, + "tier_probabilities": {"C0": 1.0, "C1": 0.0, "C2": 0.0, "C3": 0.0}, + }) + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _config: classifier) storage = SessionStorage(":memory:") await storage.connect() try: @@ -996,8 +1030,12 @@ async def test_persisted_compacted_archive_rehydrates_explicit_attachment_id() - config = GatewayConfig(llm={"provider": "openrouter"}) config.squilla_router.vision_history_lookback_turns = 0 + selector = ModelSelector(SelectorConfig(primary=ProviderConfig( + provider="openrouter", model=config.llm.model, + api_key="synthetic-offline", base_url="https://openrouter.ai/api/v1", + ))) runner = TurnRunner( - provider_selector=MagicMock(), + provider_selector=selector, session_manager=manager, config=config, ) @@ -1007,13 +1045,15 @@ async def test_persisted_compacted_archive_rehydrates_explicit_attachment_id() - current_prompt, key, provider, - None, + selector, [], "system", [{"resourceRef": {"type": "attachment", "id": attachment_id}}], semantic_message=current_prompt, + bound_user_message_id=current_entry.message_id, ) - assert returned_provider is provider + assert returned_provider.model == config.squilla_router.tiers["c0"]["model"] + assert turn.metadata["routed_tier"] == "c0" assert turn.metadata["image_intent_attachment_ids"] == [attachment_id] assert turn.metadata["image_context_has_images"] is True agent = Agent( @@ -1266,7 +1306,10 @@ async def test_non_image_attachment_id_does_not_force_vision_route() -> None: async def test_image_followup_routes_vision_and_replays_inline_history_image() -> None: manager = _FakeSessionManager() key = "agent:main:image-followup-inline" - config = GatewayConfig(llm={"provider": "openrouter"}) + config = GatewayConfig( + llm={"provider": "openrouter"}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) runner = TurnRunner(provider_selector=MagicMock(), session_manager=manager, config=config) await manager.create(key) await manager.append_message(key, "user", _inline_image_envelope("Describe this image.")) @@ -1340,7 +1383,10 @@ async def test_image_context_fact_routes_without_an_auxiliary_semantic_call( has_image_context: bool, message: str, ) -> None: - config = GatewayConfig(llm={"provider": "openrouter"}) + config = GatewayConfig( + llm={"provider": "openrouter"}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) provider = _CapturingProvider() ctx = TurnContext( message=message, @@ -1458,7 +1504,10 @@ async def test_default_sticky_window_keeps_third_followup_active() -> None: async def test_recent_image_context_survives_the_legacy_sticky_window() -> None: manager = _FakeSessionManager() key = "agent:main:image-followup-sticky-expired" - config = GatewayConfig(llm={"provider": "openrouter"}) + config = GatewayConfig( + llm={"provider": "openrouter"}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) config.squilla_router.vision_history_lookback_turns = 4 config.squilla_router.vision_sticky_followup_turns = 1 runner = TurnRunner(provider_selector=MagicMock(), session_manager=manager, config=config) diff --git a/tests/test_engine/test_model_policy_quality_retry.py b/tests/test_engine/test_model_policy_quality_retry.py new file mode 100644 index 0000000000..3857916a42 --- /dev/null +++ b/tests/test_engine/test_model_policy_quality_retry.py @@ -0,0 +1,700 @@ +from __future__ import annotations + +import copy +from collections.abc import AsyncIterator, Sequence +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from opensquilla.engine import Agent, AgentConfig, ToolResult +from opensquilla.engine.runtime import _SelectorFallbackProvider +from opensquilla.provider import ChatConfig, Message, ToolDefinition, ToolInputSchema +from opensquilla.provider import DoneEvent as ProviderDone +from opensquilla.provider import ErrorEvent as ProviderError +from opensquilla.provider import TextDeltaEvent as ProviderText +from opensquilla.provider import ToolUseEndEvent as ProviderToolUseEnd +from opensquilla.provider import ToolUseStartEvent as ProviderToolUseStart +from tests.test_engine.test_model_router_execution import execution as execution + + +class _QualityProvider: + provider_name = "fake" + + def __init__(self, streams: list[list[Any]], *, accept_retry: bool = True) -> None: + self.streams = streams + self.accept_retry = accept_retry + self.calls: list[dict[str, Any]] = [] + self.quality_calls: list[dict[str, Any]] = [] + self.timeline: list[str] = [] + + def chat( + self, + messages: list[Message], + tools: list[Any] | None = None, + config: ChatConfig | None = None, + ) -> AsyncIterator[Any]: + index = len(self.calls) + self.calls.append({"messages": copy.deepcopy(messages), "config": config}) + self.timeline.append("chat") + events = self.streams[index] if index < len(self.streams) else self.streams[-1] + return self._stream(events) + + async def _stream(self, events: list[Any]) -> AsyncIterator[Any]: + for event in events: + yield event + + async def prepare_quality_retry( + self, + reason: str, + scope_id: str, + remaining_budget: int, + messages: Sequence[Message], + chat_config: ChatConfig | None = None, + tools: list[ToolDefinition] | None = None, + context_overflow_threshold: float | None = None, + ) -> dict[str, Any] | None: + self.quality_calls.append( + { + "reason": reason, + "scope_id": scope_id, + "remaining_budget": remaining_budget, + "messages": copy.deepcopy(messages), + "chat_config": chat_config, + "tools": tools, + } + ) + self.timeline.append("quality") + if not self.accept_retry: + return None + return {"provider_id": "fake", "model_id": "quality-c2"} + + async def list_models(self) -> list[Any]: + return [] + + +def _tool_stream(index: int, *, value: str | None = None) -> list[Any]: + tool_use_id = f"tool-{index}" + return [ + ProviderToolUseStart(tool_use_id=tool_use_id, tool_name="echo"), + ProviderToolUseEnd( + tool_use_id=tool_use_id, + tool_name="echo", + arguments={"value": str(index) if value is None else value}, + ), + ProviderDone(stop_reason="tool_use", input_tokens=3, output_tokens=1), + ] + + +def _final_stream() -> list[Any]: + return [ProviderText(text="done"), ProviderDone(input_tokens=5, output_tokens=1)] + + +def _agent( + provider: _QualityProvider, + *, + budget: int = 1, + validation_reason: str | None = "invalid_tool_arguments", + fixed_route: bool = True, + explicit_correction: bool = False, + repeat_threshold: int = 0, +) -> Agent: + metadata: dict[str, Any] = {} + if fixed_route: + metadata.update(router_tier_policy="model", routing_applied=True, routed_tier="c1") + if explicit_correction: + metadata["_fixed_four_tier_explicit_correction"] = True + + async def tool_handler(call: Any) -> ToolResult: + return ToolResult( + tool_use_id=call.tool_use_id, + tool_name=call.tool_name, + content="invalid arguments" if validation_reason else "tool ok", + is_error=validation_reason is not None, + execution_status=( + { + "reason": validation_reason, + "source": "tool_runtime", + "timed_out": False, + "truncated": False, + "exit_code": None, + "preservation_class": "diagnostic", + "status": "error", + "version": 1, + } + if validation_reason is not None + else None + ), + ) + + return Agent( + provider=provider, + config=AgentConfig( + model_id="quality-c1", + provider_id="fake", + metadata=metadata, + max_iterations=4, + max_provider_retries=budget, + retry_base_backoff_ms=0, + retry_max_backoff_ms=0, + repeated_tool_call_recovery_threshold=repeat_threshold, + repeated_tool_call_recovery_extra_tools=("echo",), + ), + tool_definitions=[ + ToolDefinition( + name="echo", + description="Echo.", + input_schema=ToolInputSchema( + properties={"value": {"type": "string"}}, + required=["value"], + ), + ) + ], + tool_handler=tool_handler, + ) + + +def _routed_agent(execution, provider): + agent = _agent(provider) + agent.provider = _SelectorFallbackProvider( + provider, execution.selector, execution.turn.metadata + ) + agent.config.metadata = execution.turn.metadata + agent.config.model_id = execution.turn.model + agent.config.provider_id = "openrouter" + agent.config.thinking = False + return agent + + +@pytest.mark.parametrize( + "reason", + [ + "invalid_arguments", + "retryable_tool_input_error", + "schema_validation_failed", + "output_validation_failed", + "invalid_tool_arguments", + "invalid_json", + "format_validation_failed", + ], +) +async def test_real_agent_validation_failure_calls_quality_retry_once(reason: str) -> None: + provider = _QualityProvider([_tool_stream(1), _tool_stream(2), _final_stream()]) + agent = _agent(provider, validation_reason=reason) + + events = [event async for event in agent.run_turn("hello")] + + assert len(provider.quality_calls) == 1 + call = provider.quality_calls[0] + assert call["reason"] == "validation_failure" + assert call["remaining_budget"] == 1 + assert call["scope_id"] + assert any( + getattr(block, "type", "") == "tool_result" + for message in call["messages"] + if isinstance(message.content, list) + for block in message.content + ) + assert agent.config.model_id == "quality-c2" + assert len(provider.calls) == 3 + assert any(event.kind == "done" and event.text == "done" for event in events) + + +async def test_quality_retry_admits_the_same_assembled_messages_as_dispatch() -> None: + provider = _QualityProvider([_tool_stream(1), _final_stream()]) + agent = _agent(provider) + agent.config.request_context_prompt = "request-only context" + + events = [event async for event in agent.run_turn("hello")] + + admitted = provider.quality_calls[0] + assert admitted["messages"] == provider.calls[1]["messages"] + contents = str(admitted["messages"]) + assert "request-only context" in contents + assert "Runtime context for this turn" in contents + assert admitted["tools"] == agent.tool_definitions + assert any(event.kind == "done" for event in events) + + +@pytest.mark.parametrize("context_kind", ["request", "runtime"]) +async def test_quality_retry_large_transient_context_does_not_commit_or_activate( + execution, monkeypatch, context_kind, +) -> None: + import opensquilla.engine.model_router_quality as quality + + e = execution + e.turn.config.llm.thinking = "off" + e.turn.config.llm.max_tokens = 256 + e.turn.config.squilla_router.auto_thinking = False + before = copy.deepcopy(e.turn.metadata["router_model_decision"]) + advance = AsyncMock(wraps=e.manager.advance_model_router_quality_retry) + monkeypatch.setattr(e.manager, "advance_model_router_quality_retry", advance) + provider = _QualityProvider([_tool_stream(1), _final_stream()]) + agent = _routed_agent(e, provider) + wrapper = agent.provider + agent.config.context_window_tokens = 64000 + agent.config.context_window_known = True + agent.config.max_tokens = 256 + large_context = "extra context " * 1500 + if context_kind == "request": + agent.config.request_context_prompt = large_context + else: + monkeypatch.setattr(agent, "_runtime_context_block", lambda: large_context) + monkeypatch.setattr( + quality, "resolve_effective_context_window", lambda *a, **k: (4096, "catalog"), + ) + # A broken early admission must fail locally, never call a live provider. + monkeypatch.setattr(type(e.provider), "chat", lambda *a, **k: provider._stream(_final_stream())) + projected = [] + project = quality.project_provider_final_request + + def capture(candidate, messages, tools, config): + result = project(candidate, messages, tools, config) + small = project(candidate, [Message(role="user", content="hello")], tools, config) + projected.append((copy.deepcopy(messages), result, small)) + assert config.thinking is False + assert config.thinking_budget_tokens == 0 + # This tool failure crossed the barrier since the preceding physical + # request; preview must already see the current iteration's policy. + assert wrapper._image_probe_forbidden is True + return result + + monkeypatch.setattr(quality, "project_provider_final_request", capture) + events = [event async for event in agent.run_turn("hello")] + + assert len(projected) == 1 + messages, projection, small_projection = projected[0] + assert large_context.strip() in str(messages) + assert small_projection is not None and small_projection.fits + assert projection is not None and not projection.fits + advance.assert_not_awaited() + assert e.turn.metadata["router_model_decision"] == before + assert e.selector.current_config.model == e.turn.model == "qwen/qwen3.7-flash" + assert wrapper._provider is provider + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"] == before + assert len(provider.calls) == 2 + assert any(event.kind == "done" for event in events) + + +async def test_real_agent_no_progress_calls_quality_retry_once() -> None: + provider = _QualityProvider( + [ + _tool_stream(1, value="same"), + _tool_stream(2, value="same"), + _tool_stream(3, value="same"), + _final_stream(), + ] + ) + agent = _agent(provider, validation_reason=None, repeat_threshold=2) + + events = [event async for event in agent.run_turn("hello")] + + assert [call["reason"] for call in provider.quality_calls] == ["no_progress"] + assert any( + event.kind == "warning" and event.code == "repeated_tool_call_recovery" for event in events + ) + assert any(event.kind == "done" and event.text == "done" for event in events) + + +@pytest.mark.parametrize("synthetic", [False, True]) +async def test_repeated_tools_with_real_writes_remain_productive(monkeypatch, synthetic): + provider = _QualityProvider( + [*[_tool_stream(index, value="same") for index in range(3)], _final_stream()] + ) + agent = _agent(provider, validation_reason=None, repeat_threshold=2) + writes = [] + original_handler = agent.tool_handler + + async def write_tool(call): + result = await original_handler(call) + name = "debug_marker" if synthetic else "result" + writes.append({"relative_path": f"{name}_{len(writes)}.txt", "created": True}) + return result + + agent.tool_handler = write_tool + monkeypatch.setattr(agent, "_workspace_write_records", lambda: writes) + events = [event async for event in agent.run_turn("hello")] + + assert len(writes) == (1 if synthetic else 3) + assert len(provider.quality_calls) == int(synthetic) + assert any( + event.kind == "warning" and event.code == "repeated_tool_call_recovery" + for event in events + ) is synthetic + assert any(event.kind == "done" and event.text == "done" for event in events) + + +@pytest.mark.parametrize("code", ["429", "timeout"]) +async def test_provider_infrastructure_errors_do_not_call_quality_retry(code: str) -> None: + provider = _QualityProvider( + [ + [ + ProviderError( + message="upstream unavailable", + code=code, + ) + ], + _final_stream(), + ] + ) + + events = [event async for event in _agent(provider).run_turn("hello")] + + assert provider.quality_calls == [] + assert events + + +@pytest.mark.parametrize( + "options,metadata", + [ + pytest.param({"budget": 0}, {}, id="zero-budget"), + pytest.param( + {"fixed_route": False, "explicit_correction": True}, {}, id="unapplied-policy" + ), + pytest.param({}, {"router_control_hold_applied": True}, id="held-tier"), + pytest.param({}, {"routed_tier": "c3"}, id="top-tier"), + ], +) +async def test_quality_upgrade_requires_budget_and_an_unheld_applied_route(options, metadata): + provider = _QualityProvider([_tool_stream(1), _final_stream()]) + agent = _agent(provider, **options) + agent.config.metadata.update(metadata) + + events = [event async for event in agent.run_turn("hello")] + + assert provider.quality_calls == [] + assert agent.config.model_id == "quality-c1" + assert any(event.kind == "done" and event.text == "done" for event in events) + + +async def test_explicit_correction_is_already_resolved_by_model_policy() -> None: + provider = _QualityProvider([_final_stream()]) + agent = _agent(provider, explicit_correction=True) + agent.set_history( + [ + Message(role="user", content="previous request"), + Message(role="assistant", content="previous answer"), + ] + ) + + events = [event async for event in agent.run_turn("Your answer is wrong; correct it.")] + + assert provider.timeline == ["chat"] + assert provider.quality_calls == [] + assert any(event.kind == "done" and event.text == "done" for event in events) + + +async def test_refused_quality_retry_is_not_repeated_within_one_user_turn() -> None: + provider = _QualityProvider( + [_tool_stream(1), _tool_stream(2), _final_stream()], + accept_retry=False, + ) + agent = _agent(provider) + + events = [event async for event in agent.run_turn("hello")] + + assert len(provider.quality_calls) == 1 + assert agent.config.model_id == "quality-c1" + assert any(event.kind == "done" and event.text == "done" for event in events) + + +async def test_quality_retry_consumes_same_budget_as_later_provider_retry() -> None: + provider = _QualityProvider( + [ + _tool_stream(1), + [ + ProviderError( + message="upstream unavailable", + code="429", + ) + ], + _final_stream(), + ] + ) + agent = _agent(provider, budget=1) + + events = [event async for event in agent.run_turn("hello")] + + assert len(provider.quality_calls) == 1 + assert len(provider.calls) == 2 + assert any( + event.kind == "error" and event.code == "provider_retry_budget_exhausted" + for event in events + ) + + +async def test_transport_retry_consumes_budget_before_later_quality_signal(): + provider = _QualityProvider( + [ + [ProviderError(message="busy", code="429")], + _tool_stream(1), + _final_stream(), + ] + ) + events = [event async for event in _agent(provider, budget=1).run_turn("hello")] + assert provider.quality_calls == [] + assert events + + +async def test_repeated_failed_tools_do_not_claim_successful_no_progress(): + provider = _QualityProvider( + [_tool_stream(1, value="same"), _tool_stream(2, value="same"), _final_stream()] + ) + events = [ + event + async for event in _agent( + provider, validation_reason="permission_denied", repeat_threshold=2 + ).run_turn("hello") + ] + assert provider.quality_calls == [] + assert not any( + event.kind == "warning" and event.code == "repeated_tool_call_recovery" for event in events + ) + + +async def test_local_preflight_repair_keeps_first_dispatch_with_zero_retry_budget(): + from tests.test_engine.test_agent_image_capability_guard import ( + _image_message, + _PreflightRejectProvider, + ) + + provider = _PreflightRejectProvider(code="unsupported_image") + agent = Agent( + provider=provider, + config=AgentConfig( + max_iterations=1, + max_provider_retries=0, + model_id="custom-model", + model_vision_support="unknown", + metadata={"router_tier_policy": "model", "routing_applied": True, "routed_tier": "c1"}, + ), + ) + events = [ + event + async for event in agent.run_turn( + "Describe this image.", + extra_messages=[_image_message()], + ) + ] + assert len(provider.calls) == 1 + assert any(event.kind == "done" for event in events) + assert not any(event.kind == "error" for event in events) + + +@pytest.mark.parametrize("vision_support", ["supported", "unsupported", "unknown"]) +async def test_quality_retry_projects_the_same_physical_leg_as_dispatch( + execution, monkeypatch, vision_support, +) -> None: + import opensquilla.engine.model_router_quality as quality + from opensquilla.engine.model_policy_recovery import ModelPolicyRecovery + from opensquilla.provider.types import ContentBlockImage, ContentBlockText + + e = execution + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + from opensquilla.provider.image_projection import ImageMarkerState + + # A forced marker on the old deployment must not carry over to a new + # vision-capable candidate, while the turn-wide unknown-probe barrier must. + wrapper.configure_image_request_projection( + force_marker=True, marker_state=ImageMarkerState.NOT_ANALYZED, + forbid_unknown_probe=True, reason="configured_marker_fallback", + ) + monkeypatch.setattr( + quality.shared_catalog(), "resolve_deployment_vision_support", + lambda *a, **k: vision_support, + ) + provider_type = type(e.provider) + monkeypatch.setattr( + provider_type, "requires_complete_reasoning_history", lambda *a, **k: True, + ) + monkeypatch.setattr(provider_type, "can_replay_reasoning", lambda *a, **k: False) + messages = [ + Message(role="user", content="previous request"), + Message(role="assistant", content="previous reply without native reasoning"), + Message(role="user", content=[ + ContentBlockText(text="retry with this image"), + ContentBlockImage(media_type="image/png", data="c3ludGhldGlj"), + ]), + ] + config = ChatConfig(max_tokens=1024, thinking=True) + projected = [] + dispatched = [] + project = quality.project_provider_final_request + + def capture_projection(provider, request, tools, bound): + projected.append((copy.deepcopy(request), bound)) + # Previewing a candidate must not change live routing metadata. + assert "image_input_marker_count" not in e.turn.metadata + assert not e.turn.metadata.get("reasoning_replay_context_rebuilt") + return project(provider, request, tools, bound) + + async def chat(provider, request, tools=None, config=None): + dispatched.append((copy.deepcopy(request), config)) + yield ProviderText(text="done") + yield ProviderDone(input_tokens=1, output_tokens=1) + + monkeypatch.setattr(quality, "project_provider_final_request", capture_projection) + monkeypatch.setattr(provider_type, "chat", chat) + recovery = ModelPolicyRecovery(AgentConfig( + model_id=e.turn.model, metadata=e.turn.metadata, max_provider_retries=1, + )) + updates = await recovery.prepare( + wrapper, "validation_failure", messages, chat_config=config, + ) + assert updates is not None + admitted_messages, admitted_config = projected[0] + assert admitted_messages != messages + assert "Recorded conversation context" in str(admitted_messages) + has_images = any( + isinstance(block, ContentBlockImage) + for message in admitted_messages if isinstance(message.content, list) + for block in message.content + ) + assert has_images is (vision_support == "supported") + events = [event async for event in wrapper.chat(messages, config=admitted_config)] + assert events + assert dispatched == [(admitted_messages, admitted_config)] + + +async def test_quality_retry_uses_compacted_request_instead_of_original_history( + execution, monkeypatch, +) -> None: + import opensquilla.engine.model_router_quality as quality + from opensquilla.engine.steps.model_router import ModelRouteError, validate_model_route_capacity + + e = execution + e.turn.config.llm.thinking = "off" + e.turn.config.llm.max_tokens = 1024 + e.turn.config.squilla_router.auto_thinking = False + e.turn.config.squilla_router.tiers["c1"]["model"] = "openai/gpt-4o" + # The original history fits C0's window, but exceeds C1's smaller window. + # Agent now holds the admitted compacted view, not these entry-time estimates. + e.turn.metadata.update( + routing_history_capacity_estimated_tokens=150_000, + routing_history_capacity_message_count=100, + routing_history_capacity_estimate_complete=True, + ) + validate_model_route_capacity(e.turn, tier="c0") + with pytest.raises(ModelRouteError, match="cannot fit"): + validate_model_route_capacity(e.turn, tier="c1") + provider = _QualityProvider([_tool_stream(1), _final_stream()]) + agent = _routed_agent(e, provider) + agent.config.max_tokens = 1024 + agent.set_history([Message(role="user", content="Compacted previous task summary.")]) + projected = [] + dispatched = [] + project = quality.project_provider_final_request + + def capture(candidate, messages, tools, config): + projection = project(candidate, messages, tools, config) + assert projection is not None and projection.fits + projected.append(projection.payload) + return projection + + async def candidate_chat(candidate, messages, tools=None, config=None): + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"]["state"]["tier"] == "c1" + projection = project(candidate, messages, tools, config) + assert projection is not None and projection.fits + dispatched.append(projection.payload) + for event in _final_stream(): + yield event + + monkeypatch.setattr(quality, "project_provider_final_request", capture) + monkeypatch.setattr(type(e.provider), "chat", candidate_chat) + events = [event async for event in agent.run_turn("hello")] + + assert len(provider.calls) == 1 + assert len(dispatched) == 1 + assert dispatched == projected + assert e.selector.current_config.model == "openai/gpt-4o" + assert e.turn.metadata["router_model_decision"]["quality_retry"]["used"] is True + assert any(event.kind == "done" and event.text == "done" for event in events) + + +@pytest.mark.parametrize("call_limit", [1, 2]) +async def test_quality_retry_checks_call_budget_before_durable_upgrade( + execution, monkeypatch, call_limit, +) -> None: + + e = execution + e.turn.config.llm.thinking = "off" + e.turn.config.squilla_router.auto_thinking = False + before = copy.deepcopy(e.turn.metadata["router_model_decision"]) + advance = AsyncMock(wraps=e.manager.advance_model_router_quality_retry) + monkeypatch.setattr(e.manager, "advance_model_router_quality_retry", advance) + provider = _QualityProvider([_tool_stream(1), _final_stream()]) + agent = _routed_agent(e, provider) + wrapper = agent.provider + agent.config.max_turn_llm_calls = call_limit + dispatched = [] + + async def candidate_chat(candidate, messages, tools=None, config=None): + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"]["state"]["tier"] == "c1" + dispatched.append(messages) + for event in _final_stream(): + yield event + + monkeypatch.setattr(type(e.provider), "chat", candidate_chat) + events = [event async for event in agent.run_turn("hello")] + + assert len(provider.calls) == 1 + rows = await e.manager.get_model_router_transcript(e.node.session_key) + durable = rows[0].turn_context["router_model_decision"] + if call_limit == 1: + advance.assert_not_awaited() + assert not dispatched + assert durable == before == e.turn.metadata["router_model_decision"] + assert e.selector.current_config.model == e.turn.model == "qwen/qwen3.7-flash" + assert wrapper._provider is provider + assert any( + event.kind == "error" and event.code == "turn_llm_call_budget_exceeded" + for event in events + ) + else: + advance.assert_awaited_once() + assert len(dispatched) == 1 + assert durable["state"]["tier"] == "c1" + assert durable["quality_retry"]["used"] is True + assert any(event.kind == "done" and event.text == "done" for event in events) + + +@pytest.mark.parametrize("window_kind", ["unknown", "authority_limit"]) +async def test_quality_retry_retains_deployment_capacity_boundaries( + execution, monkeypatch, window_kind, +) -> None: + from types import SimpleNamespace + + import opensquilla.engine.model_router_quality as quality + + e = execution + e.turn.config.llm.thinking = "off" + e.turn.config.llm.max_tokens = 1024 + e.turn.config.squilla_router.auto_thinking = False + before = copy.deepcopy(e.turn.metadata["router_model_decision"]) + if window_kind == "unknown": + monkeypatch.setattr( + quality, "resolve_effective_context_window", lambda *a, **k: (128_000, "default"), + ) + else: + monkeypatch.setattr( + quality.shared_catalog(), "resolve_deployment_limits", lambda *a, **k: SimpleNamespace( + context_window=4096, max_output_tokens=1024, max_output_tokens_known=True, + ), + ) + advance = AsyncMock(wraps=e.manager.advance_model_router_quality_retry) + monkeypatch.setattr(e.manager, "advance_model_router_quality_retry", advance) + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + updates = await wrapper.prepare_quality_retry( + reason="validation_failure", scope_id="capacity-boundary", remaining_budget=1, + messages=[Message(role="user", content="large current request " * 3000)], + chat_config=ChatConfig(max_tokens=1024, thinking=False), tools=[], + ) + + assert updates is None + advance.assert_not_awaited() + assert e.selector.current_config.model == e.turn.model == "qwen/qwen3.7-flash" + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"] == before diff --git a/tests/test_engine/test_model_provider_recovery.py b/tests/test_engine/test_model_provider_recovery.py new file mode 100644 index 0000000000..e590c06330 --- /dev/null +++ b/tests/test_engine/test_model_provider_recovery.py @@ -0,0 +1,405 @@ +"""Compatibility recovery stays inside the model policy's physical-call budget.""" +from __future__ import annotations + +import json +from copy import deepcopy + +import httpx +import pytest + +from opensquilla.engine import Agent, AgentConfig +from opensquilla.engine.runtime import _SelectorFallbackProvider +from opensquilla.engine.subagent import SubagentSpec +from opensquilla.engine.types import ErrorEvent, TextDeltaEvent +from opensquilla.provider.openai import OpenAIProvider +from opensquilla.provider.openai_codex import OpenAICodexProvider +from opensquilla.provider.selector import ModelSelector, ProviderConfig, SelectorConfig +from opensquilla.provider.types import ChatConfig + + +class _TimeoutBody(httpx.AsyncByteStream): + def __init__(self, prefix=b""): + self.prefix = prefix + + async def __aiter__(self): + if self.prefix: + yield self.prefix + raise httpx.ReadTimeout("offline read timeout") + + +def _success(): + return httpx.Response(200, json={ + "choices": [{"message": {"content": "recovered"}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 2, "completion_tokens": 1}, + }) + + +def _agent(monkeypatch, tmp_path, handler, *, provider="moonshot", retries=3, child=False): + auth_path = tmp_path / "codex-auth.json" + auth_path.write_text(json.dumps({"tokens": { + "access_token": "synthetic-old", "refresh_token": "synthetic-refresh", + "account_id": "synthetic-account", + }}), encoding="utf-8") + real_client = httpx.AsyncClient + + def client(*args, **kwargs): + kwargs["transport"] = httpx.MockTransport(handler) + return real_client(*args, **kwargs) + + monkeypatch.setattr(httpx, "AsyncClient", client) + + def build(config): + if config.provider == "openai_codex": + return OpenAICodexProvider(model=config.model, auth_path=str(auth_path)) + return OpenAIProvider( + api_key="synthetic-key", model=config.model, provider_kind=config.provider, + base_url="https://offline.invalid/v1", + ) + + monkeypatch.setattr("opensquilla.provider.selector._build_provider", build) + model = "gpt-5.2-codex" if provider == "openai_codex" else "kimi-for-coding" + selector = ModelSelector(SelectorConfig(primary=ProviderConfig( + provider=provider, model=model, api_key="synthetic-key", + ))) + metadata = { + "router_tier_policy": "model", "routing_applied": True, "routed_tier": "c1", + "router_model_state": {"tier": "c1"}, + "router_model_quality_retry_callback": lambda **_: pytest.fail("parent quality callback"), + } + wrapper = _SelectorFallbackProvider(selector.resolve(), selector, metadata) + parent = Agent(wrapper, AgentConfig( + provider_id=provider, model_id=model, metadata=metadata, + max_provider_retries=retries, retry_base_backoff_ms=0, retry_max_backoff_ms=0, + )) + agent = ( + parent._make_child_agent(SubagentSpec(task="Reply briefly"), depth=1) if child else parent + ) + assert agent.provider._config_for_active_leg(ChatConfig()).physical_attempt_limit == 1 + if child: + assert agent.config.metadata == {} + assert agent.provider._turn_metadata == {} + assert agent.provider.coordinates_physical_retries + assert parent.config.metadata is metadata + return agent + + +@pytest.mark.parametrize("child", [False, True]) +@pytest.mark.parametrize("failure", ["timeout", "empty_eof", "empty_stop"]) +async def test_budgeted_nonstream_recovery(monkeypatch, tmp_path, child, failure): + calls = [] + + def handler(request): + payload = json.loads(request.content) + assert "compatibility_recovery" not in payload + assert "physical_attempt_limit" not in payload + calls.append(payload["stream"]) + if not payload["stream"]: + return _success() + if failure == "timeout": + return httpx.Response(200, stream=_TimeoutBody()) + content = b"" if failure == "empty_eof" else ( + b'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n' + ) + return httpx.Response(200, content=content) + + agent = _agent( + monkeypatch, tmp_path, handler, child=child, + provider="openrouter" if failure == "timeout" else "moonshot", + ) + events = [event async for event in agent.run_turn("Reply briefly")] + assert calls == [True, False] + assert "".join(e.text for e in events if isinstance(e, TextDeltaEvent)) == "recovered" + assert not any(isinstance(e, ErrorEvent) for e in events) + + +@pytest.mark.parametrize("child", [False, True]) +@pytest.mark.parametrize("outcome", ["success", "refresh_failure", "still_unauthorized"]) +async def test_budgeted_codex_refresh(monkeypatch, tmp_path, child, outcome): + calls = [] + + def handler(request): + if request.url.host == "auth.openai.com": + calls.append("refresh") + if outcome == "refresh_failure": + return httpx.Response(400, json={"error": "invalid_grant"}) + return httpx.Response(200, json={"access_token": "synthetic-new"}) + token = request.headers["authorization"] + calls.append("new" if token == "Bearer synthetic-new" else "old") + if calls[-1] == "old" or outcome == "still_unauthorized": + return httpx.Response(401, json={"error": {"message": "expired"}}) + return httpx.Response(200, content=( + b'data: {"type":"response.output_text.delta","delta":"recovered"}\n\n' + b'data: {"type":"response.completed","response":{"id":"r","usage":{}}}\n\n' + )) + + agent = _agent(monkeypatch, tmp_path, handler, provider="openai_codex", child=child) + events = [event async for event in agent.run_turn("Reply briefly")] + assert calls == (["old", "refresh"] if outcome == "refresh_failure" + else ["old", "refresh", "new"]) + errors = [e for e in events if isinstance(e, ErrorEvent)] + if outcome == "success": + assert not errors + assert "".join(e.text for e in events if isinstance(e, TextDeltaEvent)) == "recovered" + else: + assert len(errors) == 1 and errors[0].failure_kind == "auth_invalid" + + +@pytest.mark.parametrize("child", [False, True]) +@pytest.mark.parametrize("provider", ["moonshot", "openai_codex"]) +async def test_zero_budget_forbids_protocol_retry_and_token_refresh( + monkeypatch, tmp_path, child, provider, +): + calls = [] + + def handler(request): + calls.append(str(request.url)) + assert request.url.host != "auth.openai.com" + return httpx.Response(401, json={"error": "expired"}) if provider == "openai_codex" else ( + httpx.Response(200, content=b"") + ) + + agent = _agent(monkeypatch, tmp_path, handler, provider=provider, retries=0, child=child) + events = [event async for event in agent.run_turn("Reply briefly")] + assert len(calls) == 1 + assert any(isinstance(e, ErrorEvent) for e in events) + + +@pytest.mark.parametrize("child", [False, True]) +async def test_nonstream_transport_failure_cannot_escape_shared_budget( + monkeypatch, tmp_path, child, +): + calls = [] + + def handler(request): + streaming = json.loads(request.content)["stream"] + calls.append(streaming) + if streaming: + return httpx.Response(200, content=b"") + raise httpx.ConnectError("offline connection failure", request=request) + + async def no_wait(_delay): + pass + + monkeypatch.setattr("opensquilla.engine.agent.sleep_before_retry", no_wait) + agent = _agent(monkeypatch, tmp_path, handler, child=child) + events = [event async for event in agent.run_turn("Reply briefly")] + assert calls == [True, False, False, False] + errors = [e for e in events if isinstance(e, ErrorEvent)] + assert len(errors) == 1 and errors[0].failure_kind == "transport_transient" + + +@pytest.mark.parametrize("field", ["content", "reasoning_content"]) +async def test_nonstream_recovery_does_not_replay_visible_output(monkeypatch, tmp_path, field): + calls = [] + + def handler(request): + calls.append(json.loads(request.content)["stream"]) + prefix = ("data: " + json.dumps({"choices": [{"delta": {field: "partial"}}]}) + "\n\n") + return httpx.Response(200, stream=_TimeoutBody(prefix.encode())) + + agent = _agent(monkeypatch, tmp_path, handler) + events = [event async for event in agent.run_turn("Reply briefly")] + assert calls == [True] + assert any(isinstance(e, ErrorEvent) for e in events) + + +async def test_compatibility_recovery_shares_the_turn_budget_across_tool_calls( + monkeypatch, tmp_path, +): + from opensquilla.engine.types import ToolResult + from opensquilla.provider.types import ToolDefinition, ToolInputSchema + + calls = [] + executed = [] + + def handler(request): + streaming = json.loads(request.content)["stream"] + calls.append(streaming) + if streaming: + return httpx.Response(200, content=b"") + return httpx.Response(200, json={"choices": [{ + "message": {"tool_calls": [{ + "id": "call-1", "type": "function", + "function": {"name": "ping", "arguments": "{}"}, + }]}, "finish_reason": "tool_calls", + }]}) + + async def handle_tool(call): + executed.append(call.tool_use_id) + return ToolResult( + tool_use_id=call.tool_use_id, tool_name=call.tool_name, content="ok", + ) + + base = _agent(monkeypatch, tmp_path, handler, retries=1) + agent = Agent(base.provider, base.config, tool_handler=handle_tool, tool_definitions=[ + ToolDefinition(name="ping", description="Ping", input_schema=ToolInputSchema()), + ]) + events = [event async for event in agent.run_turn("Use ping then answer")] + assert calls == [True, False, True] + assert executed == ["call-1"] + assert not [e for e in events if e.kind == "tool_result" and e.is_error] + errors = [e for e in events if isinstance(e, ErrorEvent)] + assert len(errors) == 1 and errors[0].code == "provider_retry_budget_exhausted" + + +async def test_child_budget_is_independent_without_parent_route_state(monkeypatch, tmp_path): + calls = [] + + def handler(request): + streaming = json.loads(request.content)["stream"] + calls.append(streaming) + return httpx.Response(200, content=b"") if streaming else _success() + + parent = _agent(monkeypatch, tmp_path, handler, retries=1) + parent_events = [event async for event in parent.run_turn("Parent task")] + parent_state = deepcopy(parent.config.metadata) + child = parent._make_child_agent(SubagentSpec(task="Child task"), depth=1) + child_events = [event async for event in child.run_turn("Child task")] + assert calls == [True, False, True, False] + assert not any(isinstance(e, ErrorEvent) for e in parent_events + child_events) + assert child.config.metadata == {} and child.provider._turn_metadata != parent_state + assert child.config.max_provider_retries == 1 + assert parent.config.metadata == parent_state + + +async def test_refreshed_codex_stream_replay_does_not_refresh_again(monkeypatch, tmp_path): + from opensquilla.contracts.turn_execution import ( + SurfaceCapabilities, + TurnExecutionContext, + TurnIdentity, + ) + from opensquilla.engine.types import AnswerGenerationResetEvent + + calls = [] + + def handler(request): + if request.url.host == "auth.openai.com": + calls.append("refresh") + return httpx.Response(200, json={"access_token": "synthetic-new"}) + token = request.headers["authorization"] + calls.append("new" if token == "Bearer synthetic-new" else "old") + if calls[-1] == "old": + return httpx.Response(401, json={"error": "expired"}) + if calls.count("new") == 1: + return httpx.Response(200, stream=_TimeoutBody( + b'data: {"type":"response.output_text.delta","delta":"partial"}\n\n', + )) + return httpx.Response(200, content=( + b'data: {"type":"response.output_text.delta","delta":"recovered"}\n\n' + b'data: {"type":"response.completed","response":{"id":"r","usage":{}}}\n\n' + )) + + base = _agent(monkeypatch, tmp_path, handler, provider="openai_codex") + context = TurnExecutionContext.create( + TurnIdentity("retry-turn", "retry-answer", "agent:main:retry"), + surface=SurfaceCapabilities( + supports_streaming=True, supports_edit=True, + supports_generation_reset=True, supports_provider_call_reset=True, + ), + ) + agent = Agent(base.provider, base.config, execution_context=context) + events = [event async for event in agent.run_turn("Reply briefly")] + assert calls == ["old", "refresh", "new", "new"] + assert not any(isinstance(e, ErrorEvent) for e in events) + assert sum(isinstance(e, AnswerGenerationResetEvent) for e in events) == 1 + + +async def test_child_preserves_physical_credential_failure_attribution(monkeypatch, tmp_path): + from types import SimpleNamespace + + from opensquilla.provider.failures import ProviderFailureKind + + reported = [] + + def report_failure(provider, session, kind, **_kwargs): + reported.append((provider, session, kind)) + + monkeypatch.setattr( + "opensquilla.gateway.llm_runtime.profile_credential_pools", + lambda: SimpleNamespace(report_failure=report_failure), + ) + parent = _agent( + monkeypatch, tmp_path, lambda _: httpx.Response(401, json={"error": "invalid key"}), + ) + parent.config.metadata.update( + credential_pool={"provider": "moonshot", "session_key": "parent-pool-session"}, + routed_provider_applied="moonshot", + ) + child = parent._make_child_agent(SubagentSpec(task="Child task"), depth=1) + assert child.provider._turn_metadata["credential_pool"] == ( + parent.config.metadata["credential_pool"] + ) + assert child.provider._turn_metadata["credential_pool"] is not ( + parent.config.metadata["credential_pool"] + ) + assert "router_model_state" not in child.provider._turn_metadata + assert "router_model_quality_retry_callback" not in child.provider._turn_metadata + events = [event async for event in child.run_turn("Child task")] + assert any(isinstance(e, ErrorEvent) for e in events) + assert reported == [("moonshot", "parent-pool-session", ProviderFailureKind.AUTH_INVALID)] + + +@pytest.mark.parametrize("child", [False, True]) +@pytest.mark.parametrize("failure", ["empty", "timeout"]) +async def test_account_uncertain_failure_cannot_retry_parent_or_child(monkeypatch, child, failure): + from opensquilla import desktop_account as bridge + + lease = "os_local_" + "x" * 43 + calls = [] + state = bridge.pin_account_state({ + "source": "account", "selectedAccountId": "account", "accountId": "account", + "ready": True, "credential": lease, + }) + monkeypatch.setattr(bridge, "_leases", {lease: "http://127.0.0.1:12345"}) + + def upstream(request): + calls.append(request) + assert request.url.host == "127.0.0.1" + assert request.headers["authorization"] == "Bearer " + lease + assert json.loads(request.content)["stream"] is True + return httpx.Response(200, content=b"") if failure == "empty" else ( + httpx.Response(200, stream=_TimeoutBody()) + ) + + monkeypatch.setattr( + httpx, "AsyncHTTPTransport", lambda **_kwargs: httpx.MockTransport(upstream), + ) + try: + selector = ModelSelector(SelectorConfig(primary=ProviderConfig( + provider="tokenrhythm", model="qwen3.7-max", api_key="manual-key", + ))) + metadata = {"router_tier_policy": "model", "routing_applied": True, "routed_tier": "c1"} + parent = Agent( + _SelectorFallbackProvider(selector.resolve(), selector, metadata), + AgentConfig(provider_id="tokenrhythm", model_id="qwen3.7-max", metadata=metadata), + ) + agent = ( + parent._make_child_agent(SubagentSpec(task="Child task"), depth=1) if child else parent + ) + events = [event async for event in agent.run_turn("Reply briefly")] + finally: + bridge.reset_account_state(state) + assert len(calls) == 1 + errors = [event for event in events if isinstance(event, ErrorEvent)] + assert len(errors) == 1 and errors[0].failure_kind == "bad_request" + + +async def test_account_no_replay_decision_clears_compatibility_hint(monkeypatch): + from opensquilla.provider.types import ErrorEvent as ProviderErrorEvent + + provider = OpenAIProvider( + api_key="os_local_" + "x" * 43, model="qwen3.7-max", provider_kind="tokenrhythm", + ) + + async def uncertain(*_args): + yield ProviderErrorEvent( + message="empty response", code="incomplete_stream", compatibility_recovery="non_stream", + ) + + # Exercise the outer account policy even if a compatibility implementation + # returns a recovery hint. Its no-replay business decision remains final. + monkeypatch.setattr(provider, "_stream_impl", uncertain) + events = [event async for event in provider._stream([], None, ChatConfig())] + assert len(events) == 1 + assert events[0].code == "account_request_uncertain" + assert events[0].compatibility_recovery is None diff --git a/tests/test_engine/test_model_router_execution.py b/tests/test_engine/test_model_router_execution.py new file mode 100644 index 0000000000..049a1de2ba --- /dev/null +++ b/tests/test_engine/test_model_router_execution.py @@ -0,0 +1,657 @@ +from __future__ import annotations + +from copy import deepcopy +from types import SimpleNamespace + +import pytest + +from opensquilla.engine.model_policy_recovery import ModelPolicyRecovery +from opensquilla.engine.runtime import TurnRunner, _SelectorFallbackProvider +from opensquilla.engine.steps import squilla_router +from opensquilla.engine.types import AgentConfig, ThinkingLevel +from opensquilla.gateway.config import GatewayConfig +from opensquilla.provider.selector import ModelSelector, ProviderConfig, SelectorConfig +from opensquilla.provider.types import ChatConfig, Message +from opensquilla.session.manager import SessionManager +from opensquilla.session.storage import SessionStorage + + +class ScriptedClassifier: + def __init__(self, tier="c0"): + self.tier = tier + + def predict(self, value): + return SimpleNamespace( + intent_probabilities={"continue": 0.0, "redo": 0.0, "new_task": 1.0}, + tier_probabilities={f"c{i}": float(self.tier == f"c{i}") for i in range(4)}, + ) + + +@pytest.fixture +async def execution(monkeypatch, request): + phase = getattr(request, "param", "full") + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: ScriptedClassifier()) + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:model-execution") + entry = await manager.append_message(node.session_key, "user", "Hello") + config = GatewayConfig( + llm={ + "provider": "openrouter", + "model": "deepseek/deepseek-v4-pro", + "api_key": "synthetic-offline", + "thinking": "high", + }, + squilla_router={"rollout_phase": phase}, + llm_ensemble={"enabled": phase != "observe"}, + ) + selector = ModelSelector( + SelectorConfig( + primary=ProviderConfig( + provider="openrouter", + model=config.llm.model, + api_key="synthetic-offline", + base_url="https://openrouter.ai/api/v1", + ), + fallbacks=[ + ProviderConfig( + provider="openrouter", + model="synthetic-forbidden-fallback", + api_key="synthetic-offline", + base_url="https://openrouter.ai/api/v1", + ) + ], + ) + ) + runner = TurnRunner(provider_selector=selector, session_manager=manager, config=config) + turn, provider = await runner._run_pipeline( + "Hello", + node.session_key, + selector.resolve(), + selector, + [], + "system", + [], + bound_user_message_id=entry.message_id, + expected_session_id=node.session_id, + expected_session_epoch=0, + ) + yield SimpleNamespace( + turn=turn, + provider=provider, + selector=selector, + runner=runner, + manager=manager, + storage=storage, + node=node, + entry=entry, + ) + + +@pytest.mark.asyncio +async def test_applied_model_route_is_single_and_removes_configured_fallback(execution): + e = execution + assert e.turn.metadata["routed_tier"] == "c0" + assert [item.model for item in e.selector.remaining_chain()] == ["qwen/qwen3.7-flash"] + assert not e.selector.has_fallback() + assert type(e.provider).__name__ != "EnsembleProvider" + assert e.runner._resolve_turn_thinking(e.turn) is ThinkingLevel.HIGH + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + assert ( + wrapper._config_for_active_leg(ChatConfig(physical_attempt_limit=4)).physical_attempt_limit + == 1 + ) + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"]["state"]["tier"] == "c0" + + +@pytest.mark.asyncio +async def test_real_quality_callback_rebinds_once_and_persists_effective_tier(execution): + e = execution + original = deepcopy(e.turn.metadata["router_model_decision"]["decision"]) + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + wrapper._last_model_chat_config = ChatConfig(max_tokens=1024) + wrapper._last_model_tools = [] + recovery = ModelPolicyRecovery( + AgentConfig( + model_id=e.turn.model, + metadata=e.turn.metadata, + max_provider_retries=3, + ) + ) + messages = [Message(role="user", content="Hello")] + updates = await recovery.prepare(wrapper, "validation_failure", messages) + assert updates is not None + assert updates["model_id"] == "deepseek/deepseek-v4-flash" + assert e.turn.metadata["routed_tier"] == "c1" + assert not e.selector.has_fallback() + rows = await e.manager.get_model_router_transcript(e.node.session_key) + durable = rows[0].turn_context["router_model_decision"] + assert durable["decision"] == original + assert durable["state"]["tier"] == "c1" + assert durable["quality_retry"]["used"] is True + assert durable["execution_update"]["model"] == updates["model_id"] + assert await recovery.prepare(wrapper, "validation_failure", messages) is None + # A reconstructed helper after restart must respect the durable one-use flag. + restarted = ModelPolicyRecovery( + AgentConfig( + model_id=e.turn.model, + metadata=e.turn.metadata, + max_provider_retries=3, + ) + ) + assert await restarted.prepare(wrapper, "validation_failure", messages) is None + + +@pytest.mark.asyncio +async def test_quality_callback_refuses_unprojectable_request_without_mutation( + execution, monkeypatch +): + e = execution + import opensquilla.engine.model_router_quality as quality + + monkeypatch.setattr(quality, "project_provider_final_request", lambda *args: None) + callback = e.turn.metadata["router_model_quality_retry_callback"] + before = deepcopy(e.turn.metadata["router_model_decision"]) + updates = await callback( + reason="validation_failure", + scope_id="test-scope", + remaining_budget=1, + messages=[Message(role="user", content="Hello")], + chat_config=ChatConfig(max_tokens=1024), + tools=[], + activate_prepared=_SelectorFallbackProvider( + e.provider, + e.selector, + e.turn.metadata, + ).activate_prepared_deployment, + ) + assert updates is None + assert e.turn.metadata["router_model_decision"] == before + assert e.selector.current_config.model == "qwen/qwen3.7-flash" + + +@pytest.mark.asyncio +async def test_quality_callback_cannot_activate_after_session_reset(execution): + from opensquilla.session.storage import StaleEpochError + + e = execution + callback = e.turn.metadata["router_model_quality_retry_callback"] + await e.storage.advance_reset_epoch(e.node.session_key) + with pytest.raises(StaleEpochError): + await callback( + reason="validation_failure", + scope_id="stale-worker", + remaining_budget=1, + messages=[Message(role="user", content="Hello")], + chat_config=ChatConfig(max_tokens=1024), + tools=[], + activate_prepared=_SelectorFallbackProvider( + e.provider, + e.selector, + e.turn.metadata, + ).activate_prepared_deployment, + ) + assert e.selector.current_config.model == "qwen/qwen3.7-flash" + assert e.turn.metadata["routed_tier"] == "c0" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("execution", ["observe"], indirect=True) +async def test_observe_preserves_baseline_model_fallbacks_and_effort(execution): + from opensquilla.engine.types import ThinkingLevel + + e = execution + assert e.turn.metadata["routing_applied"] is False + assert e.selector.current_config.model == "deepseek/deepseek-v4-pro" + assert e.selector.has_fallback() + assert e.runner._resolve_turn_thinking(e.turn) == ThinkingLevel.HIGH + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + assert ( + wrapper._config_for_active_leg(ChatConfig(physical_attempt_limit=4)).physical_attempt_limit + == 4 + ) + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert "router_model_decision" not in (rows[0].turn_context or {}) + + +@pytest.mark.asyncio +async def test_failed_selected_deployment_keeps_audit_but_not_active_state(execution, monkeypatch): + from opensquilla.engine.steps.model_router import ModelRouteError + from opensquilla.session.model_router_state import build_model_router_context + + e = execution + previous = deepcopy(e.turn.metadata["router_model_decision"]["state"]) + invalid = await e.manager.append_message(e.node.session_key, "user", "inspect this image") + + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: ScriptedClassifier("c2")) + with pytest.raises(ModelRouteError, match="image support"): + await e.runner._run_pipeline( + "inspect this image", + e.node.session_key, + e.selector.resolve(), + e.selector, + [], + "system", + [{"mime_type": "image/png"}], + bound_user_message_id=invalid.message_id, + expected_session_id=e.node.session_id, + expected_session_epoch=0, + model_router_context={"router_model_state": previous}, + ) + next_input = await e.manager.append_message(e.node.session_key, "user", "continue") + rows = await e.manager.get_model_router_transcript(e.node.session_key) + rejected = next(row for row in rows if row.message_id == invalid.message_id) + audit = rejected.turn_context["router_model_decision"] + assert audit["status"] == "staged" + assert audit["state_committed"] is False + assert audit["decision"]["tier"] == "c2" + recovered = build_model_router_context( + rows, + bound_user_message_id=next_input.message_id, + exclude_last_user=True, + ) + assert recovered["router_model_state"] == previous + assert e.selector.current_config.model == "qwen/qwen3.7-flash" + + +@pytest.mark.asyncio +async def test_missing_cross_provider_credentials_does_not_admit_wrong_model( + execution, + monkeypatch, +): + from opensquilla.engine import selector_override + from opensquilla.engine.steps.model_router import ModelRouteError + from opensquilla.session.model_router_state import build_model_router_context + + e = execution + previous = deepcopy(e.turn.metadata["router_model_decision"]["state"]) + config = e.runner._config.squilla_router + config.cross_provider_tiers = True + config.tiers["c0"] = {"model": "qwen3.7-flash", "provider": "tokenrhythm"} + monkeypatch.setattr(selector_override, "resolve_tier_provider_config", lambda *a, **k: None) + entry = await e.manager.append_message(e.node.session_key, "user", "next request") + with pytest.raises(ModelRouteError, match="deployment could not be resolved"): + await e.runner._run_pipeline( + "next request", + e.node.session_key, + e.selector.resolve(), + e.selector, + [], + "system", + [], + bound_user_message_id=entry.message_id, + expected_session_id=e.node.session_id, + expected_session_epoch=0, + model_router_context={"router_model_state": previous}, + ) + following = await e.manager.append_message(e.node.session_key, "user", "continue") + rows = await e.manager.get_model_router_transcript(e.node.session_key) + rejected = next(row for row in rows if row.message_id == entry.message_id) + audit = rejected.turn_context["router_model_decision"] + assert audit["status"] == "staged" + assert audit["state_committed"] is False + assert ( + build_model_router_context( + rows, + bound_user_message_id=following.message_id, + exclude_last_user=True, + )["router_model_state"] + == previous + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cross_provider", [False, True]) +@pytest.mark.parametrize("explicit_proof_cap", [0, 777777]) +@pytest.mark.parametrize("overflow_threshold", [0.6, 0.85]) +async def test_quality_retry_activates_exact_preflight_and_thinking_budget( + execution, monkeypatch, cross_provider, explicit_proof_cap, overflow_threshold, +): + import opensquilla.engine.model_router_quality as quality + from opensquilla.engine import Agent + from opensquilla.engine.types import THINKING_BUDGETS, ThinkingLevel + from opensquilla.gateway.config import LlmProviderProfile + + e = execution + if cross_provider: + e.turn.config.squilla_router.cross_provider_tiers = True + e.turn.config.squilla_router.tiers["c1"] = { + "provider": "tokenrhythm", + "model": "deepseek-v4-flash-0731", + } + e.turn.config.llm_profiles["tokenrhythm"] = LlmProviderProfile( + provider="tokenrhythm", api_key="synthetic-offline" + ) + target = e.turn.config.squilla_router.tiers["c1"] + # Model-policy execution remains direct even with a static ensemble enabled. + assert e.turn.config.llm_ensemble.enabled is True + e.turn.config.llm_ensemble.selection_mode = "static" + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + config = AgentConfig( + model_id=e.turn.model, + metadata=e.turn.metadata, + max_provider_retries=3, + thinking=e.runner._resolve_turn_thinking(e.turn), + thinking_budget_tokens=999 if explicit_proof_cap else -1, + max_tokens=1024, + context_window_tokens=32000, + context_window_known=False, + model_vision_support="supported", + provider_request_proof_max_chars=explicit_proof_cap, + context_overflow_threshold=overflow_threshold, + ) + agent = Agent(provider=wrapper, config=config) + wrapper._last_model_chat_config = agent._provider_admission_chat_config( + "Hi", context_window_tokens=config.context_window_tokens + ) + wrapper._last_model_tools = [] + projected = [] + project = quality.project_provider_final_request + + def capture(provider, messages, tools, config): + projected.append((provider, config)) + return project(provider, messages, tools, config) + + monkeypatch.setattr(quality, "project_provider_final_request", capture) + # A second resolution could construct a different physical adapter. + monkeypatch.setattr(e.selector, "resolve", lambda: pytest.fail("resolved after preflight")) + recovery = ModelPolicyRecovery(config) + updates = await recovery.prepare( + wrapper, "validation_failure", [Message(role="user", content="Hi")] + ) + assert updates is not None + assert len(projected) == 1 + admitted_provider, admitted_config = projected[0] + assert wrapper._provider is admitted_provider + assert e.selector.current_config.provider == target["provider"] + assert e.selector.current_config.model == target["model"] + assert e.turn.metadata["router_tier_provider_role"] == "direct" + assert e.turn.metadata["routed_provider"] == target["provider"] + assert e.turn.metadata["requested_provider"] == target["provider"] + assert e.turn.metadata["requested_model"] == target["model"] + assert e.turn.metadata["resolved_model"] == target["model"] + assert e.turn.metadata["selector_execution_chain"] == [ + {"provider": target["provider"], "model": target["model"]} + ] + if cross_provider: + assert e.turn.metadata["routed_provider_applied"] == "tokenrhythm" + level = ThinkingLevel(admitted_config.thinking_level) + assert admitted_config.thinking_budget_tokens == ( + 999 if explicit_proof_cap else THINKING_BUDGETS[level] + ) + # The actual next Agent request must retain the admitted budget, including + # a user-specified budget that takes priority over automatic tier effort. + for key, value in updates.items(): + setattr(config, key, value) + assert config.resolve_thinking() == ( + level != ThinkingLevel.OFF, + admitted_config.thinking_budget_tokens, + ) + actual_config = agent._provider_admission_chat_config( + "Hi", context_window_tokens=config.context_window_tokens + ) + for field in ( + "max_tokens", "provider_context_window_tokens", "model_capabilities", + "model_vision_support", "thinking", "thinking_level", "thinking_budget_tokens", + "thinking_budget_explicit", "provider_request_max_chars", + "provider_request_max_chars_explicit_cap", + ): + assert getattr(actual_config, field) == getattr(admitted_config, field), field + + +@pytest.mark.asyncio +async def test_quality_retry_missing_credentials_keeps_admitted_state(execution, monkeypatch): + from opensquilla.engine import selector_override + + e = execution + e.turn.config.squilla_router.cross_provider_tiers = True + e.turn.config.squilla_router.tiers["c1"] = { + "provider": "tokenrhythm", + "model": "deepseek-v4-flash-0731", + } + monkeypatch.setattr(selector_override, "resolve_tier_provider_config", lambda *a, **k: None) + before = deepcopy(e.turn.metadata["router_model_decision"]) + wrapper = _SelectorFallbackProvider(e.provider, e.selector, e.turn.metadata) + wrapper._last_model_chat_config = ChatConfig(max_tokens=1024) + wrapper._last_model_tools = [] + recovery = ModelPolicyRecovery(AgentConfig(metadata=e.turn.metadata, max_provider_retries=3)) + result = await recovery.prepare( + wrapper, "validation_failure", [Message(role="user", content="Hi")] + ) + assert result is None + assert recovery.used == 0 + assert e.turn.metadata["router_model_decision"] == before + assert e.turn.metadata["routed_tier"] == "c0" + assert e.selector.current_config.model == "qwen/qwen3.7-flash" + assert wrapper._provider is e.provider + rows = await e.manager.get_model_router_transcript(e.node.session_key) + assert rows[0].turn_context["router_model_decision"] == before + + +@pytest.mark.asyncio +async def test_quality_retry_returns_to_global_provider_when_tier_omits_provider(execution): + from opensquilla.gateway.config import LlmProviderProfile + + e = execution + e.turn.config.squilla_router.cross_provider_tiers = True + e.turn.config.squilla_router.tiers["c0"] = { + "provider": "tokenrhythm", "model": "qwen3.7-flash", + } + e.turn.config.squilla_router.tiers["c1"].pop("provider") + e.turn.config.llm_profiles["tokenrhythm"] = LlmProviderProfile( + provider="tokenrhythm", api_key="synthetic-offline" + ) + entry = await e.manager.append_message(e.node.session_key, "user", "next request") + turn, provider = await e.runner._run_pipeline( + "next request", e.node.session_key, e.selector.resolve(), e.selector, + [], "system", [], bound_user_message_id=entry.message_id, + expected_session_id=e.node.session_id, expected_session_epoch=0, + model_router_context={ + "router_model_state": e.turn.metadata["router_model_decision"]["state"] + }, + ) + assert e.selector.current_config.provider == "tokenrhythm" + assert turn.metadata["routed_provider"] == "tokenrhythm" + wrapper = _SelectorFallbackProvider(provider, e.selector, turn.metadata) + wrapper._last_model_chat_config = ChatConfig(max_tokens=1024) + wrapper._last_model_tools = [] + recovery = ModelPolicyRecovery(AgentConfig(metadata=turn.metadata, max_provider_retries=3)) + + updates = await recovery.prepare( + wrapper, "validation_failure", [Message(role="user", content="next request")] + ) + + assert updates is not None + assert updates["provider_id"] == e.turn.config.llm.provider == "openrouter" + assert e.selector.current_config.provider == "openrouter" + assert turn.metadata["routed_provider"] == "openrouter" + assert turn.metadata["routed_tier"] == "c1" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "model,tier,auto_thinking,explicit_thinking,has_image,expected_level", + [ + (None, "c3", False, "off", False, "off"), + (None, "c3", True, "off", False, "off"), + (None, "c3", False, None, False, "off"), + (None, "c3", True, None, False, "high"), + (None, "c0", False, "high", False, "high"), + (None, "c0", True, "high", False, "high"), + ("qwen/qwen-vl-max", "c0", True, None, False, "off"), + ("qwen/qwen-vl-max", "c0", True, None, True, "off"), + ("qwen/qwen-vl-max", "c3", False, "off", False, "off"), + ("qwen/qwen-vl-max", "c3", False, "off", True, "off"), + ], +) +async def test_model_execution_respects_actual_thinking_requirement( + execution, monkeypatch, model, tier, auto_thinking, explicit_thinking, has_image, + expected_level, +): + from opensquilla.engine import Agent + from opensquilla.engine.types import THINKING_BUDGETS + from opensquilla.provider.ensemble import EnsembleProvider + + e = execution + e.turn.config.llm.thinking = explicit_thinking + e.turn.config.squilla_router.auto_thinking = auto_thinking + if model is not None: + e.turn.config.squilla_router.tiers[tier]["model"] = model + + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: ScriptedClassifier(tier)) + entry = await e.manager.append_message(e.node.session_key, "user", "Hello") + turn, provider = await e.runner._run_pipeline( + "Hello", e.node.session_key, e.selector.resolve(), e.selector, + [], "system", [{"mime_type": "image/png"}] if has_image else [], + bound_user_message_id=entry.message_id, + expected_session_id=e.node.session_id, + expected_session_epoch=0, + model_router_context={ + "router_model_state": e.turn.metadata["router_model_decision"]["state"] + }, + ) + assert turn.metadata["routed_tier"] == tier + assert not isinstance(provider, EnsembleProvider) + assert not e.selector.has_fallback() + assert turn.metadata["thinking_mode"] == ("T3" if tier == "c3" else "T0") + assert turn.metadata["router_model_decision"]["state_committed"] is True + if not auto_thinking: + assert "thinking_requested" not in turn.metadata + # Exercise the execution resolver and the actual Agent request-config builder, + # not only the controller's suggested thinking metadata. + agent = Agent( + provider=provider, + config=AgentConfig( + model_id=turn.model, + metadata=turn.metadata, + thinking=e.runner._resolve_turn_thinking(turn), + ), + ) + request = agent._provider_admission_chat_config( + "Hello", context_window_tokens=agent.config.context_window_tokens + ) + assert request.thinking is (expected_level != "off") + assert request.thinking_budget_tokens == THINKING_BUDGETS[ThinkingLevel(expected_level)] + if expected_level == "off": + assert request.thinking_level in {None, ThinkingLevel.OFF} + else: + assert request.thinking_level is ThinkingLevel(expected_level) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("vision_support", "default_tier"), + [("supported", "c0"), ("unsupported", "c0"), ("unsupported", "c1"), ("unknown", "c0")], +) +async def test_model_policy_tool_image_continues_only_the_admitted_tier( + execution, monkeypatch, vision_support, default_tier, +): + import json + from base64 import b64encode + + from opensquilla.engine import Agent, ToolResult + from opensquilla.engine.steps.model_router import ModelRouteError + from opensquilla.provider import ( + DoneEvent, + ModelCapabilities, + TextDeltaEvent, + ToolDefinition, + ToolInputSchema, + ToolUseEndEvent, + ToolUseStartEvent, + ) + from opensquilla.provider.openai import OpenAIProvider + from opensquilla.provider.protocol import count_provider_image_blocks + from opensquilla.tools.types import ToolContext + from tests.helpers.image_bytes import image_bytes + from tests.test_engine.test_selector_fallback_routed_model import _ProjectingScriptProvider + + e = execution + config = e.turn.config + config.llm.thinking = "off" + config.squilla_router.default_tier = default_tier + if vision_support == "supported": + config.squilla_router.tiers["c2"]["model"] = "qwen/qwen3.7-flash" + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: ScriptedClassifier("c2")) + entry = await e.manager.append_message(e.node.session_key, "user", "Inspect the tool image.") + model_context = await e.runner._router_previous_assistant_context( + e.node.session_key, bound_user_message_id=entry.message_id, exclude_last_user=True, + expected_session_id=e.node.session_id, expected_session_epoch=0, + ) + turn, _ = await e.runner._run_pipeline( + "Inspect the tool image.", e.node.session_key, e.selector.resolve(), e.selector, + [], "system", [], bound_user_message_id=entry.message_id, + expected_session_id=e.node.session_id, expected_session_epoch=0, + model_router_context=model_context, + ) + assert turn.metadata["routed_tier"] == "c2" + before = deepcopy(turn.metadata["router_model_decision"]) + monkeypatch.setattr( + squilla_router, "_get_strategy", + lambda _: pytest.fail("Internal image continuation must not reclassify a user task"), + ) + provider = _ProjectingScriptProvider( + provider_name="openrouter", + wire_provider=OpenAIProvider( + api_key="synthetic-offline", model=turn.model, + base_url="https://openrouter.ai/api/v1", provider_kind="openrouter", + ), + streams=[ + [ + ToolUseStartEvent(tool_use_id="load-image", tool_name="image"), + ToolUseEndEvent(tool_use_id="load-image", tool_name="image", arguments={}), + DoneEvent(stop_reason="tool_use"), + ], + [TextDeltaEvent(text="image answer"), DoneEvent()], + ], + ) + wrapper = _SelectorFallbackProvider( + provider, e.selector, turn.metadata, + image_routing_config=config, image_routing_session_key=e.node.session_key, + ) + wrapper.configure_image_continuation_catalog( + lambda *args, **kwargs: pytest.fail("Do not look for another image deployment") + ) + context = ToolContext(is_owner=True) + image_data = b64encode(image_bytes()).decode("ascii") + + async def image_tool(call): + context.tool_result_media[call.tool_use_id] = [{"mime": "image/png", "data": image_data}] + return ToolResult(call.tool_use_id, call.tool_name, "Image loaded.") + + agent = Agent( + provider=wrapper, + config=AgentConfig( + model_id=turn.model, provider_id="openrouter", metadata=turn.metadata, + max_iterations=2, max_provider_retries=0, thinking=False, max_tokens=1024, + model_vision_support=vision_support, + model_capabilities=ModelCapabilities( + supports_vision=vision_support == "supported", supports_tools=True, + ), + ), + tool_context=context, tool_handler=image_tool, + tool_definitions=[ + ToolDefinition( + name="image", description="Load an image.", input_schema=ToolInputSchema(), + ) + ], + ) + if vision_support == "supported": + events = [event async for event in agent.run_turn("Inspect the tool image.")] + assert not [event for event in events if event.kind == "error"], events + assert any(event.kind == "done" and event.text == "image answer" for event in events) + assert len(provider.calls) == 2 + assert count_provider_image_blocks(provider.calls[-1]["messages"]) == 1 + assert f"data:image/png;base64,{image_data}" in json.dumps(provider.calls[-1]["payload"]) + else: + with pytest.raises(ModelRouteError, match="Selected tier c2 has no proven image support"): + _ = [event async for event in agent.run_turn("Inspect the tool image.")] + assert len(provider.calls) == 1 + assert count_provider_image_blocks(provider.calls[0]["messages"]) == 0 + assert wrapper.active_deployment_config().model == turn.model + assert turn.metadata["routed_tier"] == "c2" + assert turn.metadata["router_model_decision"] == before + rows = await e.manager.get_model_router_transcript(e.node.session_key) + admitted = next(row for row in rows if row.message_id == entry.message_id) + assert admitted.turn_context["router_model_decision"] == before diff --git a/tests/test_engine/test_model_tier_policy.py b/tests/test_engine/test_model_tier_policy.py new file mode 100644 index 0000000000..62b2f27d44 --- /dev/null +++ b/tests/test_engine/test_model_tier_policy.py @@ -0,0 +1,296 @@ +"""Replay fixtures generated from the frozen experiment's actual state machine. + +Oracle: runtime-source-manifest.json (2026-10-20), source revision provenance +bf1306f8c41b3362952d4fc9469fb1567e1033da plus its frozen working diff. +The expected rows below were executed with that export's FixedFourTierV2Router +and apply_four_tier_squilla_control, not derived from this implementation. +""" + +from __future__ import annotations + +import copy +import json +from pathlib import Path +from typing import Any + +import pytest + +from opensquilla.engine.routing.model_policy import ( + ModelPolicyDecision, + ModelPolicyState, + decide_model_tier, + quality_retry_tier, +) + + +def cases() -> list[dict[str, Any]]: + rows = [] + for previous in (None, "c0", "c1", "c2", "c3"): + for intent in ("continue", "redo", "new_task"): + rows.append({"previous": previous, "intent": intent, "predicted": "c0"}) + for tier in ("c0", "c1", "c2", "c3"): + for text in ( + "hello", + "谢谢!", + "deploy to production", + "review the architecture", + "return JSON", + "Traceback (most recent call last): exception", + "a " * 3_001, + "```" + "x" * 1_500 + "```", + "src/one.py src/two.py", + "INFO x\n" * 300, + ): + rows.append({"predicted": tier, "message": text}) + for intent in ("continue", "redo", "new_task"): + rows.append({"previous": "c0", "intent": intent, "held": "c3"}) + rows.append( + { + "previous": "c1", + "intent": intent, + "message": "Your previous answer is wrong.", + "context": { + "context_tokens_est": 90000, + "material_estimated_tokens": 90000, + "turn_index": 8, + "tier_capabilities": {"c0": {"context_window": 50}}, + "budget": {"action": "cap", "limit_usd": 1, "spend_usd": 2, "cap_tier": "c0"}, + }, + } + ) + for event in ("new_task", "new", "redo", "regenerate"): + for previous in (None, "c1"): + rows.append({"previous": previous, "event": event}) + for text in ("/new", "/redo", "start a new task: explain", "重新回答一次"): + rows.append({"previous": "c1", "message": text}) + rows.extend( + [ + { + "previous": "c1", + "intent_probs": {"new_task": 0.3333334, "continue": 0.3333333, "redo": 0.3333333}, + "tier_probs": {"c0": 0.26, "c1": 0.25, "c2": 0.25, "c3": 0.24}, + }, + {"previous": "c1", "intent_probs": {"new_task": 0.5, "continue": 0.5, "redo": 0}}, + {"previous": "c1", "intent_probs": {"new_task": 0.5, "continue": 0, "redo": 0.5}}, + {"tier_probs": {"c0": 0.25, "c1": 0.25, "c2": 0.25, "c3": 0.25}}, + {"previous": "c2", "failure": True}, + {"failure": True}, + {"previous": "c2", "intent_probs": {"continue": 1}}, + {"previous": "c2", "intent": "new_task", "tier_probs": {"c0": 1}}, + { + "previous": "c1", + "context": {"flags_text_override": "hello", "context_tokens_est": 2000}, + "message": "production", + }, + {"previous": "c2", "context": {"context_tokens_est": 2001}}, + ] + ) + return rows + + +def fixture(row: dict[str, Any]): + prior = row.get("previous") + state = ( + ModelPolicyState(prior, "old-task", 2, 7, "old-start", "generation-start") + if prior + else None + ) + router_input = { + "current_request": row.get("message", "Please continue."), + "task_anchor": "Original task", + "history_user": ["older task", "one", "two", "three", "four"], + "previous_answer": "Previous response", + "previous_usage": {}, + "previous_outcome": "success", + "active_route_tier": prior.upper() if prior else None, + "route_history": [], + "context": {}, + "tool_state": {"available_tools": ["exec"]}, + "attachments": [], + } + intent = row.get("intent", "continue") + tier = row.get("predicted", "c0") + result = { + "intent_probabilities": row.get( + "intent_probs", + {label: float(label == intent) for label in ("new_task", "continue", "redo")}, + ), + "tier_probabilities": row.get( + "tier_probs", {label: float(label == tier) for label in ("c0", "c1", "c2", "c3")} + ), + } + return state, router_input, result + + +class Classifier: + def __init__(self, result: Any, *, failure: bool = False): + self.result = result + self.failure = failure + self.inputs = [] + + def predict(self, router_input): + self.inputs.append(copy.deepcopy(router_input)) + if self.failure: + raise RuntimeError("test inference failed") + return self.result + + +def project(decision): + return [ + decision.intent, + decision.tier, + decision.raw_tier, + decision.thinking_mode, + decision.prompt_policy, + decision.context_reset, + decision.intent_audit.to_dict(), + decision.tier_audit.to_dict(), + decision.next_state.turn_count, + decision.next_state.generation_start_input_message_id, + decision.metadata["flags"], + ] + + +# FROZEN_EXPECTED +FROZEN_EXPECTED = json.loads( + (Path(__file__).parent / "goldens" / "model_tier_policy_reference.json").read_text( + encoding="utf-8" + ) +) + + +@pytest.mark.parametrize("row,expected", list(zip(cases(), FROZEN_EXPECTED, strict=True))) +def test_frozen_cross_project_parity(row, expected): + state, router_input, result = fixture(row) + classifier = Classifier(result, failure=row.get("failure", False)) + decision = decide_model_tier( + router_input, + state, + classifier, + explicit_control=row.get("event"), + held_tier=row.get("held"), + input_message_id="current-input", + control_context=row.get("context", {}), + task_id_factory=lambda: "new-task", + ) + assert project(decision) == expected + assert ModelPolicyDecision.from_dict(decision.to_dict()) == decision + assert ModelPolicyState.from_dict(decision.next_state.to_dict()) == decision.next_state + assert len(classifier.inputs) <= 1 + + +def test_raw_input_no_mutation_or_legacy_text_truncation(): + state, router_input, result = fixture({"previous": "c1", "intent": "new_task"}) + router_input["current_request"] = "x" * 30_000 + router_input["previous_answer"] = "y" * 30_000 + original = copy.deepcopy(router_input) + classifier = Classifier(result) + decision = decide_model_tier(router_input, state, classifier, input_message_id="now") + assert router_input == original + assert classifier.inputs[0]["current_request"] == original["current_request"] + assert classifier.inputs[0]["previous_answer"] == original["previous_answer"] + assert classifier.inputs[0]["history_user"] == ["one", "two", "three", "four"] + assert decision.context_reset is False + assert decision.next_state.task_start_input_message_id == "now" + assert decision.next_state.generation_start_input_message_id == "generation-start" + + +def test_integrity_failure_is_not_swallowed_as_classifier_fallback(): + class IntegrityError(RuntimeError): + fail_closed = True + + class Broken: + def predict(self, value): + raise IntegrityError("manifest changed") + + with pytest.raises(IntegrityError): + decide_model_tier({"current_request": "hello"}, None, Broken()) + + +@pytest.mark.parametrize("reason", ["validation_failure", "no_progress"]) +@pytest.mark.parametrize( + "previous,next_tier", [("c0", "c1"), ("c1", "c2"), ("c2", "c3"), ("c3", None)] +) +def test_bounded_concrete_failure_recovery(reason, previous, next_tier): + assert quality_retry_tier(previous, reason, 1) == next_tier + assert quality_retry_tier(previous, reason, 0) is None + assert quality_retry_tier(previous, reason, 1, already_used=True) is None + assert quality_retry_tier(previous, reason, 1, held_tier="c0") is None + + +@pytest.mark.parametrize( + "reason", + ["explicit_correction", "timeout", "rate_limit", "connection_error", "authentication_error"], +) +def test_correction_and_transport_errors_cannot_upgrade(reason): + assert quality_retry_tier("c0", reason, 2) is None + + +def test_replay_rejects_tampering_and_incompatible_state(): + state, router_input, result = fixture({"previous": "c1"}) + decision = decide_model_tier(router_input, state, Classifier(result)) + payload = decision.to_dict() + payload["tier"] = "c3" + with pytest.raises(ValueError, match="checksum"): + ModelPolicyDecision.from_dict(payload) + payload = state.to_dict() + payload["policy_version"] = "future" + with pytest.raises(ValueError, match="incompatible"): + ModelPolicyState.from_dict(payload) + + +def test_frozen_controller_rule_identity(): + state, router_input, result = fixture({}) + decision = decide_model_tier(router_input, state, Classifier(result)) + assert decision.metadata["runtime_rules_sha256"] == ( + "2155db23882e40b5cef486a9f88b37b0c5f5e2c126c0cc27824f7e812575816d" + ) + + +@pytest.mark.parametrize( + "mutation", ["jump", "fabricated_hold", "skipped_probabilities", "correction_guard"] +) +def test_replay_rejects_rehashed_semantic_tampering(mutation): + import hashlib + + state, router_input, result = fixture({"previous": "c0", "intent": "redo"}) + payload = decide_model_tier(router_input, state, Classifier(result)).to_dict() + if mutation == "jump": + payload["tier"] = payload["raw_tier"] = payload["next_state"]["tier"] = "c2" + payload["tier_audit"]["final"] = "c2" + elif mutation == "fabricated_hold": + payload["metadata"]["router_control_hold_applied"] = True + elif mutation == "skipped_probabilities": + payload["tier_audit"]["probabilities"] = {"c0": 0, "c1": 1, "c2": 0, "c3": 0} + else: + payload["metadata"].pop("quality_escalation_satisfied_by_prior_policy") + payload.pop("trace_hash") + payload["trace_hash"] = hashlib.sha256( + json.dumps( + payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False + ).encode() + ).hexdigest() + with pytest.raises(ValueError): + ModelPolicyDecision.from_dict(payload) + + +@pytest.mark.parametrize("default_encoding", ["cp1252", "gbk"]) +def test_rules_load_utf8_when_platform_default_is_not_utf8(monkeypatch, default_encoding): + import yaml + + from opensquilla.engine.routing import model_policy + + rules_path = model_policy.default_bundle_dir() / "router.runtime.yaml" + expected = yaml.safe_load(rules_path.read_text(encoding="utf-8")) + original_read_text = Path.read_text + + def platform_read_text(path, encoding=None, errors=None): + return original_read_text(path, encoding=encoding or default_encoding, errors=errors) + + model_policy._rules.cache_clear() + try: + with monkeypatch.context() as context: + context.setattr(Path, "read_text", platform_read_text) + assert model_policy._rules()["flag_rules"] == expected["flag_rules"] + finally: + model_policy._rules.cache_clear() diff --git a/tests/test_engine/test_pipeline_required_steps.py b/tests/test_engine/test_pipeline_required_steps.py new file mode 100644 index 0000000000..8041bc9ebd --- /dev/null +++ b/tests/test_engine/test_pipeline_required_steps.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import pytest + +from opensquilla.engine.pipeline import TurnContext, run_pipeline + + +@pytest.mark.asyncio +@pytest.mark.parametrize("required", [False, True]) +async def test_failed_step_rolls_back_and_only_required_failure_stops_pipeline(required): + turn = TurnContext("request", "session", None, None, "model", [], "") + turn.metadata["existing"] = "preserved" + calls = [] + + async def route(ctx): + ctx.metadata["partial"] = "discarded" + raise ValueError("route unavailable") + + async def downstream(ctx): + calls.append("downstream") + return ctx + + if required: + with pytest.raises(ValueError, match="route unavailable"): + await run_pipeline(turn, [route, downstream], required_steps=("route",)) + else: + await run_pipeline(turn, [route, downstream]) + assert "partial" not in turn.metadata + assert turn.metadata["existing"] == "preserved" + assert calls == ([] if required else ["downstream"]) + record = turn.metadata["pipeline_steps"][0] + assert record.step_name == "route" + assert record.applied is False + assert record.fallback_reason == "route unavailable" diff --git a/tests/test_engine/test_preflight_compaction.py b/tests/test_engine/test_preflight_compaction.py index 1319a6220f..f7c6a66b16 100644 --- a/tests/test_engine/test_preflight_compaction.py +++ b/tests/test_engine/test_preflight_compaction.py @@ -795,7 +795,7 @@ async def test_preflight_retained_ref_uses_verified_pixel_reserve(tmp_path: Path mock_sm = MagicMock() mock_sm.compact = AsyncMock(return_value="summary") mock_sm.get_transcript = AsyncMock(return_value=[entry]) - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.attachments.media_root = str(media_root) runner = TurnRunner(provider_selector=MagicMock(), session_manager=mock_sm, config=config) await runner._maybe_preflight_compact( @@ -1409,7 +1409,8 @@ async def test_run_forwards_routed_provider_and_model_to_preflight() -> None: selector = _FakeProviderSelector() runner = TurnRunner( provider_selector=selector, - config=GatewayConfig(), + # This fixture isolates physical preflight without durable routing state. + config=GatewayConfig(squilla_router={"enabled": False}), model_catalog=ModelCatalog(), ) seen: dict[str, object] = {} diff --git a/tests/test_engine/test_provider_mismatch_veto.py b/tests/test_engine/test_provider_mismatch_veto.py index cc2dbd893b..02e9cd69e2 100644 --- a/tests/test_engine/test_provider_mismatch_veto.py +++ b/tests/test_engine/test_provider_mismatch_veto.py @@ -156,7 +156,7 @@ async def classify( def _config(mode: str, *, rollout_phase: str = "full") -> GatewayConfig: - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.llm.provider = "mainprov" config.llm.model = "dummy-base-model" config.squilla_router.enabled = True diff --git a/tests/test_engine/test_router_configured_image_policy.py b/tests/test_engine/test_router_configured_image_policy.py index 512ec37d9b..e5622466bb 100644 --- a/tests/test_engine/test_router_configured_image_policy.py +++ b/tests/test_engine/test_router_configured_image_policy.py @@ -1,4 +1,7 @@ -"""Router image policy: only the configured c0-c3 ladder may execute.""" +"""Legacy image routing: only the configured c0-c3 ladder may execute. + +The model policy's selected-deployment preflight is covered by model-router tests. +""" from __future__ import annotations @@ -47,7 +50,10 @@ def _context( *, message: str = "Describe the image.", ) -> TurnContext: - config = GatewayConfig(llm={"provider": "openrouter"}) + config = GatewayConfig( + llm={"provider": "openrouter"}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) config.squilla_router.tiers = tiers return TurnContext( message=message, diff --git a/tests/test_engine/test_router_control.py b/tests/test_engine/test_router_control.py index a95a140d33..7e43872579 100644 --- a/tests/test_engine/test_router_control.py +++ b/tests/test_engine/test_router_control.py @@ -24,6 +24,8 @@ def _router_cfg(tiers: dict) -> SquillaRouterConfig: return SquillaRouterConfig( + # Legacy hold-before-classification and image/floor reconciliation. + strategy="legacy_v4_phase3", enabled=True, rollout_phase="full", require_router_runtime=False, @@ -203,7 +205,7 @@ def fail_strategy(_cfg: object) -> object: out = await apply_squilla_router(ctx) - assert out.model == "z-ai/glm-5.3" + assert out.model == "moonshotai/kimi-k3" assert out.metadata["routing_source"] == "router_control_hold" assert out.metadata["router_control_hold_applied"] is True assert out.metadata["router_control_target_tier"] == "c3" @@ -361,11 +363,11 @@ def test_prompt_block_contains_canonical_targets_not_aliases() -> None: assert "router_control" in block assert "tier:c3" in block assert ( - '"target_id":"tier:c3","label":"glm-5.3",' + '"target_id":"tier:c3","label":"kimi-k3",' '"execution_kind":"single_model"' ) in block assert "tier:t3" not in block - assert "model:z-ai/glm-5.3" not in block + assert "model:moonshotai/kimi-k3" not in block assert "description" not in block assert "Use labels only to map explicit model-name requests" in block assert "must choose one target_id exactly" in block diff --git a/tests/test_engine/test_router_decision_record.py b/tests/test_engine/test_router_decision_record.py index 163f8959b4..4bda5e77b9 100644 --- a/tests/test_engine/test_router_decision_record.py +++ b/tests/test_engine/test_router_decision_record.py @@ -81,7 +81,8 @@ def _reset_hook_state(): def _ctx(message: str = PROMPT_SENTINEL, session_key: str = "agent:main:main") -> TurnContext: - config = GatewayConfig() + # Persistence/history tests below inject the legacy classify() protocol. + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.squilla_router.rollout_phase = "full" return TurnContext( message=message, diff --git a/tests/test_engine/test_routing_policy_parity.py b/tests/test_engine/test_routing_policy_parity.py index 96200236b9..18d91375d3 100644 --- a/tests/test_engine/test_routing_policy_parity.py +++ b/tests/test_engine/test_routing_policy_parity.py @@ -71,7 +71,7 @@ "enabled": True, "auto_thinking": True, "rollout_phase": "full", - "strategy": "v4_phase3", + "strategy": "legacy_v4_phase3", "tier_profile": None, "cross_provider_tiers": False, "default_tier": "c1", @@ -695,7 +695,7 @@ def run_case(case: Case) -> dict: sr._strategy = None sr._strategy_key = None - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) config.llm.provider = case.llm_provider config.llm.model = case.llm_model for key, value in {**BASE_ROUTER_KNOBS, **case.router}.items(): diff --git a/tests/test_engine/test_runtime_ensemble_credential_guard.py b/tests/test_engine/test_runtime_ensemble_credential_guard.py index fe46458732..d9019b4a9e 100644 --- a/tests/test_engine/test_runtime_ensemble_credential_guard.py +++ b/tests/test_engine/test_runtime_ensemble_credential_guard.py @@ -139,7 +139,7 @@ def _tokenrhythm_shared_c3_router() -> dict[str, Any]: assert preset is not None tiers = preset.tier_defaults() tiers["c3"]["ensemble_enabled"] = True - return {"preset_binding": "custom", "tiers": tiers} + return {"strategy": "legacy_v4_phase3", "preset_binding": "custom", "tiers": tiers} def _static_b5_config(**ensemble_overrides: Any) -> GatewayConfig: @@ -282,7 +282,7 @@ async def test_static_tokenrhythm_b5_wraps_when_active_provider_is_keyed( ("routed_tier", "expected_model", "expect_ensemble"), [ ("c0", "qwen3.7-flash", False), - ("c1", "deepseek-flash", False), + ("c1", "deepseek-v4-flash-0731", False), ("c2", "deepseek-v4-pro-0813", False), # Shared C3 triggers the global plan without replacing the configured # direct/fallback selector head. @@ -339,7 +339,7 @@ async def route_to_requested_tier(turn): assert provider.profile_name == "static_tokenrhythm_b5" assert provider.fallback_model == "deepseek-v4-flash-0731" assert turn.model == "deepseek-v4-flash-0731" - assert turn.metadata["routed_model_before_ensemble"] == "glm-5.3" + assert turn.metadata["routed_model_before_ensemble"] == "kimi-k3" assert turn.metadata["ensemble_activation_source"] == "router_tier" assert turn.metadata["ensemble_tier_binding"] == "shared" assert turn.metadata["ensemble_selection_mode"] == "static_tokenrhythm_b5" @@ -1995,7 +1995,7 @@ async def route_to_c3(turn): assert calls == [] assert selector.current_config.model == "deepseek-v4-flash-0731" assert turn.model == "deepseek-v4-flash-0731" - assert turn.metadata["routed_model_before_ensemble"] == "glm-5.3" + assert turn.metadata["routed_model_before_ensemble"] == "kimi-k3" assert turn.metadata["ensemble_wrap_skipped_reason"] == ( "static_tokenrhythm_b5_no_credential" ) diff --git a/tests/test_engine/test_runtime_model_generation.py b/tests/test_engine/test_runtime_model_generation.py new file mode 100644 index 0000000000..f38df45dfc --- /dev/null +++ b/tests/test_engine/test_runtime_model_generation.py @@ -0,0 +1,597 @@ +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from opensquilla.engine.agent import Agent +from opensquilla.engine.runtime import RouterHistoryReplayRequest, TurnRunner +from opensquilla.engine.types import AgentConfig +from opensquilla.gateway.config import GatewayConfig +from opensquilla.session.manager import SessionManager +from opensquilla.session.models import SessionSummary +from opensquilla.session.storage import SessionStorage + + +def envelope(mid: str, *, generation: str | None = None, admitted: bool = True) -> dict: + return { + "state_committed": admitted, + "status": "admitted" if admitted else "staged", + "input_message_id": mid, + "state": { + "tier": "c1", + "version": 1, + "task_id": mid, + "turn_count": 1, + "task_start_input_message_id": mid, + "generation_start_input_message_id": generation, + }, + } + + +@pytest.mark.asyncio +@pytest.mark.parametrize("summary_mode", [ + "unavailable", "disabled", "timeout", "cancelled", "unexpected-error", +]) +async def test_explicit_generation_uses_ephemeral_window_and_preserves_timeline( + monkeypatch, summary_mode, +): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:generation") + old = await manager.append_message(node.session_key, "user", "OLD SECRET " * 500) + reset = await manager.append_message(node.session_key, "user", "new generation " * 500) + await manager.update_message_turn_context( + node.session_key, + reset.message_id, + { + "router_model_decision": envelope(reset.message_id, generation=reset.message_id), + }, + ) + await manager.append_message(node.session_key, "assistant", "generation answer " * 500) + current = await manager.append_message(node.session_key, "user", "continue now") + await storage.save_summary( + SessionSummary( + session_id=node.session_id, + session_key=node.session_key, + summary_text="OLD CHECKPOINT SECRET", + ) + ) + before = await storage.get_canonical_transcript(node.session_id) + runner = TurnRunner( + provider_selector=MagicMock(), session_manager=manager, + config=GatewayConfig(compaction={"enabled": summary_mode != "disabled"}), + ) + from opensquilla.session.compaction_lifecycle import CompactionTimeoutError + + failures = { + "disabled": AssertionError("disabled compaction must not summarize"), + "timeout": CompactionTimeoutError("summarizing"), + "cancelled": asyncio.CancelledError(), + "unexpected-error": RuntimeError("summary accounting failed"), + } + summarizer = None + if summary_mode in failures: + summarizer = AsyncMock(side_effect=failures[summary_mode]) + monkeypatch.setattr("opensquilla.session.compaction.compact_context", summarizer) + monkeypatch.setattr( + runner, + "_load_compaction_context_records", + AsyncMock( + side_effect=AssertionError("generation must not load mixed checkpoints"), + ), + ) + + def admitted(summary, rows): + return len(summary) + sum(len(str(row.get("content") or "")) for row in rows) < 700 + + prepare = runner._maybe_preflight_compact( + node.session_key, + 200, + history_capacity_tokens=200, + history_has_persisted_user=True, + bound_user_message_id=current.message_id, + expected_session_id=node.session_id, + expected_session_epoch=0, + consumer_admission=admitted, + ) + if summary_mode in {"cancelled", "unexpected-error"}: + with pytest.raises(type(failures[summary_mode])): + await prepare + assert [row.model_dump() for row in before] == [ + row.model_dump() for row in await storage.get_canonical_transcript(node.session_id) + ] + return + window = await prepare + if summary_mode == "disabled": + summarizer.assert_not_awaited() + elif summary_mode == "timeout": + summarizer.assert_awaited_once() + assert window is not None and window.reason == "model_generation_context" + assert all("OLD SECRET" not in row.content for row in window.kept_entries) + queued = await manager.append_message(node.session_key, "user", "FUTURE SECRET") + agent = Agent( + provider=MagicMock(), + config=AgentConfig( + metadata={ + "router_tier_policy": "model", + "routing_applied": True, + } + ), + ) + summary = await runner._load_history( + agent, + node.session_key, + prepared_window=window, + bound_user_message_id=current.message_id, + expected_session_id=node.session_id, + expected_session_epoch=0, + ) + assert "OLD CHECKPOINT" not in (summary or "") + assert agent.config.metadata["router_model_prepared_window_admitted"] is True + after = await storage.get_canonical_transcript(node.session_id) + assert [row.model_dump() for row in before] == [row.model_dump() for row in after[:-1]] + assert after[-1].message_id == queued.message_id + assert all("FUTURE SECRET" not in str(message.content) for message in agent._history) + assert (await manager.get_summaries(node.session_key))[ + 0 + ].summary_text == "OLD CHECKPOINT SECRET" + assert old.message_id in { + row.message_id for row in await manager.get_transcript(node.session_key) + } + + +@pytest.mark.asyncio +async def test_staged_explicit_reset_scopes_capacity_without_changing_active_state(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:staged-generation") + old = await manager.append_message(node.session_key, "user", "old " * 2000) + await manager.update_message_turn_context( + node.session_key, + old.message_id, + { + "router_model_decision": envelope(old.message_id), + }, + ) + current = await manager.append_message(node.session_key, "user", "new task") + await manager.update_message_turn_context( + node.session_key, + current.message_id, + { + "router_model_decision": envelope( + current.message_id, generation=current.message_id, admitted=False + ), + }, + ) + runner = TurnRunner( + provider_selector=MagicMock(), session_manager=manager, config=GatewayConfig() + ) + measured = await runner._router_history_capacity_for_request( + node.session_key, + RouterHistoryReplayRequest( + exclude_last_user=True, + bound_user_message_id=current.message_id, + expected_session_id=node.session_id, + expected_session_epoch=0, + ), + max_history_turns=0, + preserve_image_attachments=False, + ) + assert measured["history_capacity_estimated_tokens"] == 0 + assert measured["history_capacity_estimate_complete"] is True + context = await runner._router_previous_assistant_context( + node.session_key, + bound_user_message_id=current.message_id, + exclude_last_user=True, + ) + assert context["router_model_state"]["task_id"] == old.message_id + + +def test_outcome_and_blank_latest_assistant_match_frozen_projection(): + from opensquilla.session.model_router_state import build_model_router_context + + rows = [ + SimpleNamespace(role="user", message_id="a", content="earlier", turn_context={}), + SimpleNamespace(role="assistant", message_id="b", content="old answer", turn_context={}), + SimpleNamespace( + role="assistant", + message_id="c", + content='{"text":"","artifacts":[]}', + turn_context={"execution_status": "failed"}, + ), + SimpleNamespace(role="user", message_id="d", content="now", turn_context={}), + ] + result = build_model_router_context( + rows, bound_user_message_id="d", exclude_last_user=True + )["router_model_input"] + assert result["previous_answer"] == "" + assert result["previous_outcome"] == "failure" + rows[2].turn_context = {} + result = build_model_router_context( + rows, bound_user_message_id="d", exclude_last_user=True + )["router_model_input"] + assert result["previous_outcome"] == "unknown" + + +def test_features_use_active_slice_until_compacted_task_anchor_requires_canonical(): + from opensquilla.session.model_router_state import build_model_router_context + + old = SimpleNamespace( + role="user", message_id="old", content="archived prior task", turn_context={} + ) + anchor = SimpleNamespace( + role="user", + message_id="anchor", + content="active task", + turn_context={"router_model_decision": envelope("anchor")}, + ) + current = SimpleNamespace( + role="user", message_id="current", content="continue", turn_context={} + ) + rows = [old, anchor, current] + options = dict(bound_user_message_id="current", exclude_last_user=True) + active = build_model_router_context(rows, feature_entries=[anchor, current], **options) + assert active["router_model_input"]["history_user"] == ["active task"] + assert active["router_model_history_user_chars"] == len("active task") + assert active["router_model_control_context"]["context_tokens_est"] > 0 + recovered = build_model_router_context(rows, feature_entries=[current], **options) + assert recovered["router_model_input"]["history_user"] == ["archived prior task", "active task"] + assert recovered["router_model_control_context"]["context_tokens_est"] == 0 + with pytest.raises(RuntimeError, match="feature boundary"): + build_model_router_context( + rows, feature_entries=[current], canonical_complete=False, **options + ) + anchor.turn_context = {} + no_state = build_model_router_context(rows, feature_entries=[current], **options) + assert no_state["router_model_input"]["history_user"] == [] + +@pytest.fixture +async def image_generation(monkeypatch): + import json + + from opensquilla.engine.steps import squilla_router + from opensquilla.provider.selector import ModelSelector, ProviderConfig, SelectorConfig + + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:generation-image") + image = {"type": "image/png", "data": "aGVsbG8="} + await manager.append_message( + node.session_key, "user", json.dumps({"text": "OLD IMAGE TASK", "attachments": [image]}) + ) + await manager.append_message(node.session_key, "assistant", "OLD IMAGE ANSWER") + config = GatewayConfig( + llm={ + "provider": "openrouter", + "model": "deepseek/deepseek-v4-flash", + "api_key": "synthetic-offline", + "thinking": "off", + } + ) + selector = ModelSelector( + SelectorConfig( + primary=ProviderConfig( + provider="openrouter", + model=config.llm.model, + api_key="synthetic-offline", + base_url="https://openrouter.ai/api/v1", + ) + ) + ) + runner = TurnRunner(provider_selector=selector, session_manager=manager, config=config) + owner = dict(expected_session_id=node.session_id, expected_session_epoch=0) + env = SimpleNamespace( + manager=manager, storage=storage, node=node, runner=runner, config=config, owner=owner + ) + + async def route(text, tier, *, new_task=False, current_image=False): + attachments = [image] if current_image else [] + content = ( + json.dumps({"text": text, "attachments": attachments}) if attachments else text + ) + entry = await manager.append_message(node.session_key, "user", content) + if new_task: + await manager.update_message_turn_context( + node.session_key, entry.message_id, {"router_model_control_event": "new_task"} + ) + context = await runner._router_previous_assistant_context( + node.session_key, + bound_user_message_id=entry.message_id, + exclude_last_user=True, + include_capacity=True, + **owner, + ) + env.context, env.entry = context, entry + classifier = SimpleNamespace( + predict=lambda _: SimpleNamespace( + intent_probabilities={"continue": 0.0, "redo": 0.0, "new_task": 1.0}, + tier_probabilities={f"c{i}": float(tier == f"c{i}") for i in range(4)}, + ) + ) + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: classifier) + return await runner._run_pipeline( + text, + node.session_key, + selector.resolve(), + selector, + [], + "system", + attachments, + model_router_context=context, + history_has_recent_image=context.get("history_has_recent_image", False), + bound_user_message_id=entry.message_id, + **owner, + router_history_replay_request=RouterHistoryReplayRequest( + exclude_last_user=True, + bound_user_message_id=entry.message_id, + **owner, + ), + ) + + env.route = route + yield env + + +@pytest.mark.asyncio +@pytest.mark.parametrize("followup", [False, True]) +async def test_explicit_new_task_drops_prior_generation_image_requirement( + image_generation, followup +): + e = image_generation + turn, _ = await e.route("Write a sorting function", "c0" if followup else "c1", new_task=True) + assert turn.metadata["router_model_decision"]["decision"]["context_reset"] is True + assert not e.context.get("history_has_recent_image", False) + assert e.context["history_capacity_estimated_tokens"] == 0 + if followup: + await e.manager.append_message(e.node.session_key, "assistant", "NEW TEXT ANSWER") + turn, _ = await e.route("Explain the code complexity", "c1") + assert turn.model == "deepseek/deepseek-v4-flash" + assert not turn.metadata["image_context_has_images"] + assert not e.context.get("history_has_recent_image", False) + agent = Agent(provider=MagicMock(), config=AgentConfig()) + summary = await e.runner._load_history( + agent, e.node.session_key, bound_user_message_id=e.entry.message_id, **e.owner + ) + expected = ( + [("user", "Write a sorting function"), ("assistant", "NEW TEXT ANSWER")] if followup else [] + ) + assert [(message.role, message.content) for message in agent._history] == expected + assert summary is None + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "image_source", ["current", "generation_history", "archived_generation_history"] +) +async def test_model_generation_still_requires_vision_for_actual_images( + image_generation, image_source +): + from opensquilla.engine.steps.model_router import ModelRouteError + + e = image_generation + if image_source != "current": + await e.route("Inspect this new image", "c0", new_task=True, current_image=True) + await e.manager.append_message(e.node.session_key, "assistant", "NEW IMAGE ANSWER") + if image_source == "archived_generation_history": + rows = await e.storage.get_transcript(e.node.session_id) + node = await e.manager.get_session(e.node.session_key) + node.compaction_count += 1 + await e.storage.rewrite_compacted_session( + node=node, + summary=SessionSummary( + session_id=node.session_id, + session_key=node.session_key, + summary_text="old checkpoint", + compaction_id="image-generation-test", + removed_count=len(rows) - 1, + kept_count=1, + covered_through_id=rows[-2].id, + ), + entries=rows[-1:], + archived_entries=rows[:-1], + expected_source_entries=rows, + **e.owner, + ) + assert ( + await e.storage.get_canonical_transcript_coverage(node.session_id) + ).canonical_complete + with pytest.raises(ModelRouteError, match="no proven image support"): + await e.route( + "Explain this", + "c1", + new_task=image_source == "current", + current_image=image_source == "current", + ) + assert e.context.get("history_has_recent_image", False) == (image_source != "current") + + +@pytest.mark.asyncio +async def test_observed_new_task_keeps_existing_image_history(image_generation): + e = image_generation + e.config.squilla_router.rollout_phase = "observe" + entry = await e.manager.append_message(e.node.session_key, "user", "Observe a new task") + await e.manager.update_message_turn_context( + e.node.session_key, entry.message_id, {"router_model_control_event": "new_task"} + ) + context = await e.runner._router_previous_assistant_context( + e.node.session_key, + bound_user_message_id=entry.message_id, + exclude_last_user=True, + **e.owner, + ) + assert context["history_has_recent_image"] is True + + +@pytest.mark.asyncio +async def test_new_default_generation_summary_preserves_facts_and_tool_pairs_on_wire( + tmp_path, monkeypatch, +): + """The offline responder can only repeat facts it actually receives.""" + import functools + import json + + import httpx + + from opensquilla.engine.routing.model_policy import ModelPolicyState + from opensquilla.engine.steps import squilla_router + from opensquilla.gateway.config import ModelOverrideConfig, SquillaRouterConfig + from opensquilla.provider import model_catalog + from opensquilla.provider.model_catalog import ModelCatalog + from opensquilla.provider.selector import ModelSelector, ProviderConfig, SelectorConfig + from opensquilla.provider.types import ModelInfo + from opensquilla.token_estimation import estimate_tokens + from opensquilla.tools.registry import ToolRegistry + from opensquilla.tools.types import ToolContext + from scripts import live_reasoning_replay_e2e as harness + + monkeypatch.setenv("OPENROUTER_API_KEY", "synthetic-openrouter") + monkeypatch.setenv("OPENSQUILLA_OPENROUTER_LIVE_PRICING", "0") + model, window = "qwen/qwen-2.5-coder-32b-instruct", 32768 + endpoint = harness.registry_endpoint("openrouter") + config = harness._config(tmp_path, "openrouter", model, endpoint) + config.llm.context_window_tokens = window + config.llm.max_tokens = 4096 + config.llm.thinking = "off" + config.compaction.enabled = True + config.compaction.protected_recent_messages = 4 + config.squilla_router = SquillaRouterConfig( + rollout_phase="full", preset_binding="custom", auto_thinking=False, + tiers={f"c{i}": {"provider": "openrouter", "model": model} for i in range(4)}, + ) + assert config.squilla_router.strategy == "lightgbm_model" + classifier = SimpleNamespace(predict=lambda _: SimpleNamespace( + intent_probabilities={"continue": 1.0, "redo": 0.0, "new_task": 0.0}, + tier_probabilities={f"c{i}": float(i == 1) for i in range(4)}, + )) + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: classifier) + catalog = ModelCatalog() + config.models = {"openrouter": {model: ModelOverrideConfig( + context_window=window, max_output_tokens=4096, + )}} + catalog._models[model] = ModelInfo( + provider="openrouter", model_id=model, display_name=model, + context_window=window, max_output_tokens=4096, + supports_reasoning=False, supports_tools=True, supports_vision=False, + ) + catalog.set_user_overrides({ + f"openrouter/{model}": config.models["openrouter"][model].model_dump(exclude_none=True), + }) + monkeypatch.setattr(model_catalog, "_shared_catalog", catalog) + facts = { + "constraint": "Do not rename public endpoints.", + "artifact": "reports/router-review.json", + "pending": "Run the SQLite migration check.", + } + seen = [] + + def respond(request): + payload = json.loads(request.content) + text = json.dumps(payload["messages"], ensure_ascii=False) + assert "OLD TASK SECRET" not in text + assert "OLD CHECKPOINT SECRET" not in text + assert "FUTURE QUEUED SECRET" not in text + available = {key: value for key, value in facts.items() if value in text} + seen.append((payload, available)) + frame = { + "id": "generation-response", "model": model, + "choices": [{"index": 0, "delta": {"content": json.dumps(available)}, + "finish_reason": "stop"}], + "usage": {"prompt_tokens": 100, "completion_tokens": 100, "total_tokens": 200}, + } + return httpx.Response( + 200, request=request, headers={"content-type": "text/event-stream"}, + content=("data: " + json.dumps(frame) + "\n\ndata: [DONE]\n\n").encode(), + ) + + observer = harness.WireObserver(endpoint, transport=httpx.MockTransport(respond), max_calls=8) + async with SessionStorage(tmp_path / "generation.sqlite") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:scoped-generation-summary") + key = node.session_key + await manager.append_message(key, "user", "OLD TASK SECRET") + await storage.save_summary(SessionSummary( + session_id=node.session_id, session_key=key, summary_text="OLD CHECKPOINT SECRET", + )) + unit = " log 17;" + repeats = 48000 // (6 * estimate_tokens(unit)) + reset = None + for index in range(6): + prefix = json.dumps({"constraint": facts["constraint"]}) + "\n" if index == 0 else "" + entry = await manager.append_message(key, "user", prefix + unit * repeats) + if index == 0: + reset = entry + decision = envelope(entry.message_id, generation=entry.message_id) + decision["state"] = ModelPolicyState( + tier="c1", task_id=entry.message_id, + task_start_input_message_id=entry.message_id, + generation_start_input_message_id=entry.message_id, + ).to_dict() + await manager.update_message_turn_context(key, entry.message_id, { + "router_model_decision": decision, + }) + # Early discovered facts live in an assistant reply, which the + # old user-prioritizing temporary window can omit under pressure. + reply = (json.dumps({key: value for key, value in facts.items() if key != "constraint"}) + if index == 0 else "Disposable logs acknowledged.") + await manager.append_message(key, "assistant", reply) + assert reset is not None + await manager.append_message(key, "user", "Inspect the current specification.") + await manager.append_message(key, "assistant", "", tool_calls=[{ + "type": "tool_use", "tool_use_id": "generation-read", "name": "read_file", + "input": {"path": "spec.txt"}, + }, { + "type": "tool_result", "tool_use_id": "generation-read", "name": "read_file", + "result": "The current specification is available.", + }]) + await manager.append_message(key, "assistant", "Specification inspected.") + original = harness._canonical_message_digests(await manager.get_canonical_transcript(key)) + prompt = "Continue: return the original constraint, artifact and pending action as JSON." + current = await manager.append_message(key, "user", prompt) + await manager.append_message(key, "user", "FUTURE QUEUED SECRET") + runner = TurnRunner( + provider_selector=ModelSelector(SelectorConfig(primary=ProviderConfig( + provider="openrouter", model=model, api_key="synthetic-openrouter", + base_url=endpoint, + ))), + tool_registry=ToolRegistry(), session_manager=manager, config=config, + model_catalog=catalog, + ) + prepared = [] + original_preflight = runner._maybe_preflight_compact + + @functools.wraps(original_preflight) + async def capture_preflight(*args, **kwargs): + candidate = await original_preflight(*args, **kwargs) + if candidate is not None: + rows = [runner._entry_for_emergency_compaction(row) + for row in candidate.kept_entries] + assert kwargs["compaction_budget"].consumer_admission(candidate.summary, rows) + prepared.append(candidate) + return candidate + + monkeypatch.setattr(runner, "_maybe_preflight_compact", capture_preflight) + with observer.observe(): + events = [event async for event in runner.run( + prompt, session_key=key, bound_user_message_id=current.message_id, + tool_context=ToolContext(is_owner=True, workspace_dir=config.workspace_dir), + )] + assert not [event for event in events if event.kind == "error"] + payload, visible_facts = seen[-1] + assert visible_facts == facts + summary_calls = [call for call in observer.calls if harness._is_compaction_wire_call(call)] + assert summary_calls, "A new generation must summarize its own early facts before trimming" + assert prepared and all(value in prepared[-1].summary for value in facts.values()) + assert estimate_tokens(json.dumps(payload)) + config.llm.max_tokens < window + calls = [call["id"] for message in payload["messages"] + for call in message.get("tool_calls", [])] + results = [message["tool_call_id"] for message in payload["messages"] + if message["role"] == "tool"] + assert calls == results == ["generation-read"] + canonical = harness._canonical_message_digests(await manager.get_canonical_transcript(key)) + assert all(canonical.get(message_id) == digest for message_id, digest in original.items()) + assert [row.summary_text for row in await manager.get_summaries(key)] == [ + "OLD CHECKPOINT SECRET", + ] diff --git a/tests/test_engine/test_runtime_routed_history_compaction.py b/tests/test_engine/test_runtime_routed_history_compaction.py index c81885fc67..a94f2c99a7 100644 --- a/tests/test_engine/test_runtime_routed_history_compaction.py +++ b/tests/test_engine/test_runtime_routed_history_compaction.py @@ -88,6 +88,7 @@ async def test_small_route_preserves_old_task_facts_before_final_projection( config.compaction.enabled = True config.preflight_compact_ratio = 0.8 config.squilla_router = SquillaRouterConfig( + strategy="legacy_v4_phase3", enabled=True, cross_provider_tiers=True, preset_binding="custom", tiers={ f"c{index}": { diff --git a/tests/test_engine/test_runtime_router_fallback.py b/tests/test_engine/test_runtime_router_fallback.py index 527d5f5765..4592d007a9 100644 --- a/tests/test_engine/test_runtime_router_fallback.py +++ b/tests/test_engine/test_runtime_router_fallback.py @@ -115,7 +115,9 @@ async def classify( def _config_with_router_timeout() -> GatewayConfig: return GatewayConfig( - squilla_router=SquillaRouterConfig(routing_timeout_seconds=0.01) + squilla_router=SquillaRouterConfig( + strategy="legacy_v4_phase3", routing_timeout_seconds=0.01, + ) ) @@ -482,7 +484,9 @@ async def classification_wait_for(awaitable, timeout): monkeypatch.setattr(runtime_module, "asyncio", SimpleNamespace( **(vars(asyncio) | {"wait_for": classification_wait_for}), )) - config = GatewayConfig(squilla_router={"enabled": False, "routing_timeout_seconds": 5}) + config = GatewayConfig(squilla_router={ + "strategy": "legacy_v4_phase3", "enabled": False, "routing_timeout_seconds": 5, + }) accepted = capture_model_routing_config(config, session_mode="router") class ReadyStrategy: @@ -687,3 +691,18 @@ async def test_pipeline_budget_refresh_awaits_exact_owner_and_preserves_known_sp ) assert turn.metadata["session_billed_cost_usd"] == (2.0 if refresh_fails else 3.0) assert turn.metadata["session_total_cost_usd"] == (2.0 if refresh_fails else 3.0) + + +@pytest.mark.asyncio +async def test_model_router_timeout_fails_closed_without_provider_execution(monkeypatch): + async def slow_router(ctx): + await asyncio.sleep(1) + return ctx + + monkeypatch.setattr("opensquilla.engine.steps.apply_squilla_router", slow_router) + runner = TurnRunner(provider_selector=None, config=GatewayConfig( + squilla_router={"routing_timeout_seconds": 0.01}, + )) + with pytest.raises(TimeoutError, match="squilla router timed out"): + await runner._run_pipeline("request", "agent:main:timeout", _Provider(), None, + [], "system", []) diff --git a/tests/test_engine/test_runtime_web_chat_timeout.py b/tests/test_engine/test_runtime_web_chat_timeout.py index 3df3544046..91163b6e20 100644 --- a/tests/test_engine/test_runtime_web_chat_timeout.py +++ b/tests/test_engine/test_runtime_web_chat_timeout.py @@ -168,7 +168,7 @@ async def _chat(*args: Any, **kwargs: Any) -> AsyncIterator[Any]: runner = TurnRunner( provider_selector=selector, session_manager=session_manager, - config=GatewayConfig(**config_kwargs), + config=GatewayConfig(squilla_router={"enabled": False}, **config_kwargs), ) async for _ in runner.run( diff --git a/tests/test_engine/test_selector_fallback_routed_model.py b/tests/test_engine/test_selector_fallback_routed_model.py index 5287a13857..c46ef83303 100644 --- a/tests/test_engine/test_selector_fallback_routed_model.py +++ b/tests/test_engine/test_selector_fallback_routed_model.py @@ -3326,7 +3326,7 @@ def _image_continuation_runtime( primary_model = "synthetic/text-small" image_model = "synthetic/image-large" - gateway = GatewayConfig() + gateway = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) gateway.llm.provider = "openrouter" gateway.llm.model = primary_model gateway.llm.api_key = "synthetic-key" diff --git a/tests/test_engine/test_turn_prompt_binding.py b/tests/test_engine/test_turn_prompt_binding.py index 00c96cc9d7..889de2bf9d 100644 --- a/tests/test_engine/test_turn_prompt_binding.py +++ b/tests/test_engine/test_turn_prompt_binding.py @@ -16,8 +16,9 @@ import io import json from collections.abc import AsyncIterator -from dataclasses import dataclass +from dataclasses import dataclass, replace from pathlib import Path +from types import SimpleNamespace from typing import Any from unittest.mock import MagicMock @@ -64,6 +65,7 @@ class _TranscriptEntry: tool_calls: list[Any] | None = None reasoning_content: str | None = None token_count: int | None = None + session_id: str = "" @dataclass @@ -81,6 +83,7 @@ def __init__(self) -> None: self._summaries: dict[str, list[SessionSummary]] = {} self._context_states: dict[str, list[SessionContextState]] = {} self._counter = 0 + self.storage = self async def create(self, session_key: str) -> _SessionNode: node = _SessionNode(session_key=session_key, session_id=f"id-{len(self._nodes) + 1}") @@ -94,9 +97,31 @@ async def append_message(self, session_key: str, role: str, content: str) -> _Tr self._transcripts.setdefault(session_key, []).append(entry) return entry - async def get_transcript(self, session_key: str) -> list[_TranscriptEntry]: + async def get_transcript( + self, session_key: str, *, expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> list[_TranscriptEntry]: + node = self._nodes[session_key] + assert expected_session_id in (None, node.session_id) + assert expected_session_epoch in (None, 0) return list(self._transcripts.get(session_key, [])) + async def get_model_router_transcript( + self, session_key: str, *, expected_session_id: str | None = None, + expected_session_epoch: int | None = None, + ) -> list[_TranscriptEntry]: + node = self._nodes[session_key] + rows = await self.get_transcript( + session_key, expected_session_id=expected_session_id, + expected_session_epoch=expected_session_epoch, + ) + return [replace(row, session_id=node.session_id) for row in rows] + + async def get_canonical_transcript_coverage(self, session_id: str) -> SimpleNamespace: + key = next(key for key, node in self._nodes.items() if node.session_id == session_id) + # This fixture keeps no archived copy after compaction. + return SimpleNamespace(canonical_complete=not self._summaries.get(key)) + async def get_session(self, session_key: str) -> _SessionNode | None: return self._nodes.get(session_key) @@ -1524,7 +1549,13 @@ async def get_transcript(self, session_key: str) -> list[_TranscriptEntry]: raise RuntimeError("synthetic transcript failure") manager = _FailingTranscriptManager() - context = await _new_runner(manager)._router_previous_assistant_context( + runner = _new_runner(manager) + with pytest.raises(RuntimeError, match="synthetic transcript failure"): + await runner._router_previous_assistant_context( + "agent:main:failed-transcript", include_capacity=True, + ) + runner._config.squilla_router.strategy = "legacy_v4_phase3" + context = await runner._router_previous_assistant_context( "agent:main:failed-transcript", include_capacity=True, ) diff --git a/tests/test_engine/test_usage_event_accounting.py b/tests/test_engine/test_usage_event_accounting.py index f920faf202..f7f142f8a9 100644 --- a/tests/test_engine/test_usage_event_accounting.py +++ b/tests/test_engine/test_usage_event_accounting.py @@ -1116,9 +1116,9 @@ def test_tokenrhythm_inline_router_c0_c3_reconciles_each_physical_request() -> N tiers = preset.tier_defaults() expected_models = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", } results: list[UsageCallResult] = [] diff --git a/tests/test_engine/turn_runner/test_input_normalization_metadata.py b/tests/test_engine/turn_runner/test_input_normalization_metadata.py index 689a3f8d2e..bc80d4a79b 100644 --- a/tests/test_engine/turn_runner/test_input_normalization_metadata.py +++ b/tests/test_engine/turn_runner/test_input_normalization_metadata.py @@ -145,7 +145,7 @@ async def test_attachment_materialization_threads_into_sanitized_pipeline_metada ) -> None: captured: dict[str, Any] = {} - async def fake_run_pipeline(ctx, steps): # noqa: ANN001, ARG001 + async def fake_run_pipeline(ctx, steps, **kwargs): # noqa: ANN001, ARG001 captured["metadata"] = ctx.metadata return ctx @@ -194,7 +194,7 @@ async def test_run_pipeline_seeds_turn_context_with_normalization_metadata( ) -> None: captured: dict[str, Any] = {} - async def fake_run_pipeline(ctx, steps): # noqa: ANN001, ARG001 + async def fake_run_pipeline(ctx, steps, **kwargs): # noqa: ANN001, ARG001 captured["metadata"] = ctx.metadata return ctx @@ -232,7 +232,7 @@ async def test_run_pipeline_ignores_non_positive_material_token_metadata( ) -> None: captured: dict[str, Any] = {} - async def fake_run_pipeline(ctx, steps): # noqa: ANN001, ARG001 + async def fake_run_pipeline(ctx, steps, **kwargs): # noqa: ANN001, ARG001 captured["metadata"] = ctx.metadata return ctx diff --git a/tests/test_engine/turn_runner/test_router_control_replay_event.py b/tests/test_engine/turn_runner/test_router_control_replay_event.py index dadade24cd..8d603a173e 100644 --- a/tests/test_engine/turn_runner/test_router_control_replay_event.py +++ b/tests/test_engine/turn_runner/test_router_control_replay_event.py @@ -102,6 +102,7 @@ async def test_router_control_replay_event_replays_turn_once(monkeypatch) -> Non cfg = GatewayConfig( llm={"provider": "openrouter"}, squilla_router=SquillaRouterConfig( + strategy="legacy_v4_phase3", enabled=True, rollout_phase="full", require_router_runtime=False, @@ -137,7 +138,7 @@ async def test_router_control_replay_event_replays_turn_once(monkeypatch) -> Non assert replay_events[0].target_tier == "c3" # Initial turn routes to c1 (the strategy returns c1); the router_control # hold replays it at c3. Models follow the default tier profile. - assert provider.calls == ["deepseek/deepseek-v4-flash-0731", "z-ai/glm-5.3"] + assert provider.calls == ["deepseek/deepseek-v4-flash", "moonshotai/kimi-k3"] assert initial_deployments == [provider.calls[0]] assert done_events[-1].text == "new final" assert text.endswith("new final") @@ -217,6 +218,7 @@ async def test_replayed_turn_keeps_user_message_binding(monkeypatch) -> None: cfg = GatewayConfig( llm={"provider": "openrouter"}, squilla_router=SquillaRouterConfig( + strategy="legacy_v4_phase3", enabled=True, rollout_phase="full", require_router_runtime=False, @@ -286,6 +288,7 @@ async def test_router_control_replay_persists_distinct_usage_executions( cfg = GatewayConfig( llm={"provider": "openrouter"}, squilla_router=SquillaRouterConfig( + strategy="legacy_v4_phase3", enabled=True, rollout_phase="full", require_router_runtime=False, @@ -323,15 +326,15 @@ async def test_router_control_replay_persists_distinct_usage_executions( # Guard the premise: the replay really ran, at two different models. assert len([e for e in events if isinstance(e, RouterControlReplayEvent)]) == 1 assert not any(isinstance(e, ErrorEvent) for e in events) - assert provider.calls == ["deepseek/deepseek-v4-flash-0731", "z-ai/glm-5.3"] + assert provider.calls == ["deepseek/deepseek-v4-flash", "moonshotai/kimi-k3"] # Both provider legs are real spend and must survive the ledger's identity # guards as separate finalized rows under one logical turn. assert len(rows) == 2 assert {row.status for row in rows} == {"finalized"} rows_by_model = {row.model: row for row in rows} - first = rows_by_model["deepseek/deepseek-v4-flash-0731"] - second = rows_by_model["z-ai/glm-5.3"] + first = rows_by_model["deepseek/deepseek-v4-flash"] + second = rows_by_model["moonshotai/kimi-k3"] assert first.turn_id is not None assert second.turn_id == first.turn_id assert first.execution_id == first.turn_id diff --git a/tests/test_engine/turn_runner/test_transcript_snapshot_runtime.py b/tests/test_engine/turn_runner/test_transcript_snapshot_runtime.py index d79ff99fcb..fc973dcfbc 100644 --- a/tests/test_engine/turn_runner/test_transcript_snapshot_runtime.py +++ b/tests/test_engine/turn_runner/test_transcript_snapshot_runtime.py @@ -182,7 +182,11 @@ async def classify( def _config() -> GatewayConfig: - return GatewayConfig(llm={"provider": "openrouter", "model": _MODEL}) + # These fixtures exercise legacy best-effort snapshot/retry semantics. + return GatewayConfig( + llm={"provider": "openrouter", "model": _MODEL}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) def _tool_context() -> ToolContext: @@ -720,6 +724,7 @@ async def _counted_get_transcript( session_manager=manager, config=GatewayConfig( llm={"provider": "openrouter", "model": _MODEL}, + squilla_router={"strategy": "legacy_v4_phase3"}, agent_max_provider_retries=1, ), ) @@ -952,6 +957,7 @@ async def _counted_get_transcript( config = GatewayConfig( llm={"provider": "openrouter", "model": _MODEL}, squilla_router=SquillaRouterConfig( + strategy="legacy_v4_phase3", enabled=True, rollout_phase="full", require_router_runtime=False, diff --git a/tests/test_gateway/test_admission_input.py b/tests/test_gateway/test_admission_input.py index bc83eb8275..ca90c3fd2d 100644 --- a/tests/test_gateway/test_admission_input.py +++ b/tests/test_gateway/test_admission_input.py @@ -303,3 +303,60 @@ def test_initial_model_identity_normalizes_aliases_but_preserves_provider_and_mo assert decode_admit_turn(base).request_fingerprint == decode_admit_turn({ **base, "initialModel": None, "initialProvider": None, }).request_fingerprint + + +@pytest.mark.parametrize("surface", ["webchat", "session"]) +@pytest.mark.parametrize("alias", ["routerControlEvent", "router_control_event"]) +@pytest.mark.parametrize("control", [None, "new_task", "redo"]) +async def test_router_control_reaches_typed_admission(surface, alias, control): + application = SimpleNamespace(admit=AsyncMock(return_value={"status": "accepted"})) + params = { + "sessionKey" if surface == "webchat" else "key": "agent:main:synthetic", + "message": "ordinary text", alias: control, + } + await GatewayTurnAdmissionAdapter(application).admit(params, surface=surface) + assert application.admit.await_args.args[0].router_control_event == control + + +@pytest.mark.parametrize("surface", ["webchat", "session"]) +@pytest.mark.parametrize("fields", [ + {"routerControlEvent": "new"}, + {"router_control_event": "continue"}, + {"routerControlEvent": " redo "}, + {"routerControlEvent": 1}, + {"routerControlEvent": []}, + {"routerControlEvent": "new_task", "router_control_event": "redo"}, + {"routerControlEvent": None, "router_control_event": "redo"}, +]) +async def test_invalid_router_control_never_reaches_admission(surface, fields): + application = SimpleNamespace(admit=AsyncMock()) + params = { + "sessionKey" if surface == "webchat" else "key": "agent:main:synthetic", + "message": "ordinary text", **fields, + } + with pytest.raises(ValueError, match="routerControlEvent"): + await GatewayTurnAdmissionAdapter(application).admit(params, surface=surface) + application.admit.assert_not_awaited() + + +def test_router_control_identity_is_alias_independent_and_binds_the_event(): + base = {"key": "agent:main:synthetic", "message": "ordinary text"} + redo = decode_admit_turn({**base, "routerControlEvent": "redo"}) + alias = decode_admit_turn({**base, "router_control_event": "redo"}) + duplicate = decode_admit_turn({ + **base, "routerControlEvent": "redo", "router_control_event": "redo", + }) + assert redo.request_fingerprint == alias.request_fingerprint == duplicate.request_fingerprint + assert redo.request_fingerprint != decode_admit_turn(base).request_fingerprint + assert redo.request_fingerprint != decode_admit_turn({ + **base, "routerControlEvent": "new_task", + }).request_fingerprint + assert decode_admit_turn(base).request_fingerprint == decode_admit_turn({ + **base, "routerControlEvent": None, + }).request_fingerprint + + +@pytest.mark.parametrize("message", ["/new", "/redo", "Start a new task", "redo the answer"]) +def test_message_text_cannot_manufacture_trusted_router_control(message): + command = decode_admit_turn({"key": "agent:main:synthetic", "message": message}) + assert command.router_control_event is None diff --git a/tests/test_gateway/test_canonical_read_admission_socket.py b/tests/test_gateway/test_canonical_read_admission_socket.py index c647d8dcc9..79bfa0b948 100644 --- a/tests/test_gateway/test_canonical_read_admission_socket.py +++ b/tests/test_gateway/test_canonical_read_admission_socket.py @@ -344,7 +344,10 @@ async def test_ordinary_history_does_not_compete_with_admitted_metadata( ): case = read_socket_case modern = await case.client() - cli = GatewayClient(request_timeout_s=2) + # The still-held metadata gate proves independence. Allow SQLite worker + # scheduling within a watchdog shorter than the gate's ten-second bound. + read_timeout_s = 5 + cli = GatewayClient(request_timeout_s=read_timeout_s) await cli.connect(case.url) case.gate.operation = "list_recent_agent_tasks" _, metadata = await modern.read(HYDRATE) @@ -360,7 +363,7 @@ async def test_ordinary_history_does_not_compete_with_admitted_metadata( # recovery budget, as do other ordinary in-process callers. history = await asyncio.wait_for(read_chat_history_v4( {"sessionKey": KEY}, RpcContext(conn_id="_http", session_manager=case.manager), - ), 2) + ), read_timeout_s) assert len(history["messages"]) == 1 assert history["canonical_available"] assert not metadata.done() and not case.release.is_set() diff --git a/tests/test_gateway/test_model_router_control_ingress.py b/tests/test_gateway/test_model_router_control_ingress.py new file mode 100644 index 0000000000..f62ffc8b48 --- /dev/null +++ b/tests/test_gateway/test_model_router_control_ingress.py @@ -0,0 +1,101 @@ +"""Structured model controls retain their accepted causal input identity.""" + +from pathlib import Path + +import pytest + +from opensquilla.gateway.rpc import get_dispatcher +from opensquilla.session.model_router_state import build_model_router_context +from tests.test_gateway.test_turn_ingress_rpc import SESSION_KEY, _open_real_stack + + +@pytest.mark.parametrize("control", ["new_task", "redo"]) +@pytest.mark.parametrize( + "method,key_field", [("chat.send", "sessionKey"), ("sessions.send", "key")], +) +@pytest.mark.parametrize("queue_mode", ["followup", "collect", "steer"]) +async def test_router_control_survives_acceptance_activation_and_replay( + tmp_path: Path, control: str, method: str, key_field: str, queue_mode: str, +) -> None: + async with _open_real_stack(tmp_path / "control.db") as stack: + params = { + key_field: SESSION_KEY, "message": "Use this exact request", + "clientRequestId": "control-request", "clientMessageId": "control-message", + "routerControlEvent": control, "queueMode": queue_mode, + } + accepted = await get_dispatcher().dispatch("accept", method, params, stack.context) + assert accepted.ok, accepted.error + await stack.wait_until_running() + entries = await stack.storage.get_canonical_transcript(stack.session_id) + users = [entry for entry in entries if entry.role == "user"] + assert len(users) == 1 + assert users[0].turn_context["router_model_control_event"] == control + router_context = build_model_router_context( + entries, bound_user_message_id=users[0].message_id, + exclude_last_user=True, + ) + assert router_context["router_model_control_event"] == control + task = await stack.storage.get_agent_task(accepted.payload["task_id"]) + assert task.queue_mode == "followup" + assert task.details["metadata"]["router_model_control_event"] == control + assert stack.received_runs[0].envelope.metadata["router_model_control_event"] == control + replay = await get_dispatcher().dispatch("replay", method, params, stack.context) + assert replay.ok, replay.error + assert replay.payload["replayed"] is True + changed = await get_dispatcher().dispatch( + "changed", method, + {**params, "routerControlEvent": "redo" if control == "new_task" else "new_task"}, + stack.context, + ) + assert not changed.ok + assert changed.error.code == "IDEMPOTENCY_CONFLICT" + + +@pytest.mark.parametrize("control", ["new_task", "redo"]) +async def test_pending_router_control_survives_staging_and_dispatch(tmp_path: Path, control: str): + async with _open_real_stack(tmp_path / "pending-control.db") as stack: + staged = await get_dispatcher().dispatch( + "stage", "sessions.pending_inputs.enqueue", { + "key": SESSION_KEY, "message": "Use this queued request", + "clientRequestId": "pending-control-request", "pendingInputId": "pending-control", + "clientMessageId": "pending-control-message", + "router_control_event": control, + }, stack.context, + ) + assert staged.ok, staged.error + assert staged.payload["routerControlEvent"] == control + accepted = await get_dispatcher().dispatch( + "dispatch", "sessions.pending_inputs.dispatch", { + "key": SESSION_KEY, "pendingInputId": "pending-control", + "clientRequestId": "pending-control-request", + "requestFingerprint": staged.payload["requestFingerprint"], + }, stack.context, + ) + assert accepted.ok, accepted.error + await stack.wait_until_running() + entries = await stack.storage.get_canonical_transcript(stack.session_id) + users = [entry for entry in entries if entry.role == "user"] + assert len(users) == 1 + assert users[0].turn_context["router_model_control_event"] == control + router_context = build_model_router_context( + entries, bound_user_message_id=users[0].message_id, + exclude_last_user=True, + ) + assert router_context["router_model_control_event"] == control + assert stack.received_runs[0].envelope.metadata["router_model_control_event"] == control + + +@pytest.mark.parametrize("control", ["new_task", "redo"]) +async def test_router_control_does_not_leak_to_cached_system_followup(tmp_path: Path, control: str): + async with _open_real_stack(tmp_path / "cached-control.db") as stack: + accepted = await get_dispatcher().dispatch( + "accept", "chat.send", { + "sessionKey": SESSION_KEY, "message": "Own this control", + "clientRequestId": "control-request", "routerControlEvent": control, + }, stack.context, + ) + assert accepted.ok, accepted.error + await stack.wait_until_running() + followup = await stack.runtime.send(SESSION_KEY, "Independent system followup") + task = await stack.storage.get_agent_task(followup.task_id) + assert "router_model_control_event" not in task.details["metadata"] diff --git a/tests/test_gateway/test_router_boot.py b/tests/test_gateway/test_router_boot.py index cc9f85a46f..4175bee303 100644 --- a/tests/test_gateway/test_router_boot.py +++ b/tests/test_gateway/test_router_boot.py @@ -2138,7 +2138,7 @@ async def fake_build_memory_managers(*_args: Any, **_kwargs: Any) -> dict[str, A await services.close() -def test_router_boot_validation_does_not_load_heavy_runtime( +def test_legacy_router_boot_validation_does_not_load_heavy_runtime( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -2147,6 +2147,7 @@ def test_router_boot_validation_does_not_load_heavy_runtime( (bundle_dir / "router.runtime.yaml").write_text("v4: {}\n", encoding="utf-8") config = GatewayConfig() + config.squilla_router.strategy = "legacy_v4_phase3" config.squilla_router.v4_bundle_dir = str(bundle_dir) config.squilla_router.require_router_runtime = True @@ -2162,8 +2163,9 @@ def guarded_import(name: str, *args: Any, **kwargs: Any) -> Any: validate_squilla_router_runtime(config) -def test_router_boot_validation_still_fails_when_required_bundle_missing(tmp_path: Path) -> None: +def test_legacy_router_boot_validation_fails_when_required_bundle_missing(tmp_path: Path) -> None: config = GatewayConfig() + config.squilla_router.strategy = "legacy_v4_phase3" config.squilla_router.v4_bundle_dir = str(tmp_path / "missing") config.squilla_router.require_router_runtime = True @@ -2820,3 +2822,48 @@ def test_explicit_full_default_keeps_sandbox_capability_for_safe_mode() -> None: assert settings.run_mode == "safe" assert settings.sandbox is True assert settings.security_grading is True + + +@pytest.mark.parametrize("saved_strategy", [None, "v4_phase3"]) +def test_model_router_boot_validates_configured_bundle_without_loading_models( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, saved_strategy: str | None, +) -> None: + from opensquilla.squilla_router import lightgbm_model + + calls = [] + router = {"strategy": saved_strategy} if saved_strategy is not None else {} + config = GatewayConfig(squilla_router=router) + assert config.squilla_router.strategy == "lightgbm_model" + config.squilla_router.lightgbm_model_dir = str(tmp_path / "frozen-model") + monkeypatch.setattr( + lightgbm_model, "validate_artifacts", + lambda root: calls.append(root), + ) + validate_squilla_router_runtime(config) + assert calls == [tmp_path / "frozen-model"] + + +def test_model_router_boot_missing_bundle_fails_closed(tmp_path: Path) -> None: + from opensquilla.squilla_router.lightgbm_model import LightGBMModelError + + config = GatewayConfig() + config.squilla_router.lightgbm_model_dir = str(tmp_path / "missing-model") + with pytest.raises(LightGBMModelError) as caught: + validate_squilla_router_runtime(config) + assert caught.value.fail_closed + + +@pytest.mark.parametrize("saved_strategy", [None, "v4_phase3"]) +def test_model_policy_migration_keeps_disabled_router_off(saved_strategy, monkeypatch): + from opensquilla.squilla_router import lightgbm_model + + router = {"enabled": False} + if saved_strategy is not None: + router["strategy"] = saved_strategy + config = GatewayConfig(squilla_router=router) + monkeypatch.setattr( + lightgbm_model, "validate_artifacts", + lambda _: pytest.fail("Disabled router must not load model resources"), + ) + validate_squilla_router_runtime(config) + assert config.squilla_router.enabled is False diff --git a/tests/test_gateway/test_router_tier_presets.py b/tests/test_gateway/test_router_tier_presets.py index 85eb6878f5..7b4e720589 100644 --- a/tests/test_gateway/test_router_tier_presets.py +++ b/tests/test_gateway/test_router_tier_presets.py @@ -448,6 +448,10 @@ def test_managed_dormant_foreign_draft_does_not_gain_unapproved_routes( # older TokenRhythm ladders and the immediate pre-refresh defaults. _PREVIOUS_MODELS = { "openrouter": ( + ( + "qwen/qwen3.7-flash", "deepseek/deepseek-v4-flash-0731", + "deepseek/deepseek-v4-pro-0813", "z-ai/glm-5.3", + ), ( "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "z-ai/glm-5.2", "z-ai/glm-5.2", @@ -458,6 +462,7 @@ def test_managed_dormant_foreign_draft_does_not_gain_unapproved_routes( ), ), "tokenrhythm": ( + ("qwen3.7-flash", "deepseek-flash", "deepseek-v4-pro-0813", "glm-5.3"), ("deepseek-v4-flash", "deepseek-v4-pro", "kimi-k2.7-code", "glm-5.1"), ("deepseek-v4-flash", "deepseek-v4-pro", "kimi-k2.7-code", "glm-5.2"), ("qwen3.7-flash", "deepseek-v4-flash-0731", "glm-5.2", "glm-5.2"), @@ -721,9 +726,9 @@ def test_full_default_tree_round_trips_via_to_toml_dict(tmp_path: Path) -> None: assert set(tiers) == {"c0", "c1", "c2", "c3", "image_model"} expected_models = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", "image_model": "kimi-k2.6", } for name, tier in tiers.items(): diff --git a/tests/test_gateway/test_setup_workflow_adapter.py b/tests/test_gateway/test_setup_workflow_adapter.py new file mode 100644 index 0000000000..da3ecb2d42 --- /dev/null +++ b/tests/test_gateway/test_setup_workflow_adapter.py @@ -0,0 +1,48 @@ +"""Setup reads must leave the Gateway loop available for shutdown and RPCs.""" + +from __future__ import annotations + +import asyncio +import threading + +from opensquilla import desktop_account +from opensquilla.gateway.adapters import setup_workflow + + +async def test_setup_status_waits_for_account_without_blocking_gateway(monkeypatch): + started = threading.Event() + release = threading.Event() + snapshot = {"source": "manual", "ready": False} + reads = [] + + def read_account(): + reads.append(True) + started.set() + # A watchdog releases the baseline's blocking implementation as well. + release.wait(2) + return snapshot + + def project_status(config, *, is_owner): + assert desktop_account.account_state() == snapshot + assert is_owner + return {"llmConfigured": False} + + monkeypatch.setattr(desktop_account, "desktop_account_available", lambda: True) + monkeypatch.setattr(desktop_account, "account_state_for_turn", read_account) + monkeypatch.setattr(setup_workflow, "_setup_status", project_status) + scope = desktop_account.pin_account_state(None) + port = setup_workflow.GatewaySetupWorkflowPort(object(), is_owner=True) + task = asyncio.create_task(port.load_setup_status()) + try: + await asyncio.sleep(0) + assert await asyncio.to_thread(started.wait, 1) + assert not task.done(), "account I/O must not block unrelated Gateway work" + assert desktop_account._turn_state.get() is None + release.set() + assert await task == {"llmConfigured": False} + assert reads == [True] + assert desktop_account._turn_state.get() is None + finally: + release.set() + await task + desktop_account.reset_account_state(scope) diff --git a/tests/test_gateway_config_default_provider.py b/tests/test_gateway_config_default_provider.py index 4f480ac7cb..e731e82707 100644 --- a/tests/test_gateway_config_default_provider.py +++ b/tests/test_gateway_config_default_provider.py @@ -179,9 +179,9 @@ def test_tokenrhythm_default_binds_router_tiers_to_tokenrhythm() -> None: tiers = cfg.squilla_router.tiers expected_models = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", "image_model": "kimi-k2.6", } for name, model in expected_models.items(): @@ -201,7 +201,7 @@ def test_tokenrhythm_curated_ladder_is_not_rebound_to_the_direct_model() -> None # packaged profiles. cfg = GatewayConfig(llm={"provider": "tokenrhythm", "model": "glm-5.2"}) assert cfg.llm.model == "glm-5.2" - assert cfg.squilla_router.tiers["c1"]["model"] == "deepseek-flash" + assert cfg.squilla_router.tiers["c1"]["model"] == "deepseek-v4-flash-0731" def test_tokenrhythm_custom_tiers_are_preserved() -> None: diff --git a/tests/test_gateway_timing.py b/tests/test_gateway_timing.py index 6c701ec424..68e448dd15 100644 --- a/tests/test_gateway_timing.py +++ b/tests/test_gateway_timing.py @@ -2,10 +2,15 @@ from __future__ import annotations +import hashlib +import json +import sys from pathlib import Path +from types import SimpleNamespace import pytest +from scripts import compare_gateway_timing as timing from scripts.compare_gateway_timing import ( sample_environment, validate_sources, @@ -63,3 +68,164 @@ def test_checkout_identity_rejects_dirty_source(tmp_path, monkeypatch): ) with pytest.raises(ValueError, match="exact clean Git checkout"): verify_source_identity(tmp_path, "a" * 40) + + +@pytest.fixture +def comparison(tmp_path, monkeypatch): + baseline, candidate = tmp_path / "baseline", tmp_path / "candidate" + for root in (baseline, candidate): + module = root / "src/opensquilla/gateway/boot.py" + module.parent.mkdir(parents=True) + module.write_text("# synthetic fixture\n", encoding="utf-8") + for name in ("uv.lock", "pyproject.toml"): + (root / name).write_text(root.name + name, encoding="utf-8") + baseline_python = tmp_path / "baseline-env/bin/python" + identities = { + str(executable): { + "version": [3, 12, 14], + "implementation": "cpython", + "machine": "x86_64", + "prefix": str(prefix), + } + for executable, prefix in ( + (baseline_python, tmp_path / "baseline-env"), + (sys.executable, tmp_path / "candidate-env"), + ) + } + monkeypatch.setattr( + timing, "_interpreter_identity", lambda executable: dict(identities[executable]) + ) + verified, calls = [], [] + monkeypatch.setattr( + timing, + "verify_source_identity", + lambda root, sha, **options: verified.append((root, sha, options)), + ) + + def sample(command, **options): + calls.append((command, options)) + scale = 1 if options["cwd"] == baseline else 2 + row = { + "startup_to_health_ms": 100 * scale, + "default_first_turn_ms": 20 * scale, + "provider_calls": 1, + "terminal_events": 1, + "goals": 0, + } + return SimpleNamespace(returncode=0, stdout="fixture log\n" + json.dumps(row), stderr="") + + monkeypatch.setattr(timing.subprocess, "run", sample) + return SimpleNamespace( + baseline=baseline, + candidate=candidate, + baseline_python=baseline_python, + identities=identities, + verified=verified, + calls=calls, + arguments={ + "baseline": baseline, + "candidate": candidate, + "baseline_sha": "a" * 40, + "candidate_sha": "b" * 40, + "samples": 3, + "baseline_python": baseline_python, + }, + ) + + +def test_compare_default_keeps_dependency_guard(comparison): + arguments = { + key: value for key, value in comparison.arguments.items() if key != "baseline_python" + } + with pytest.raises(ValueError, match="identical uv.lock"): + timing.compare(**arguments) + assert comparison.calls == [] + + +def test_separate_environments_alternate_correct_interpreters_and_record_evidence(comparison): + e = comparison + report = timing.compare(**e.arguments) + expected_order = ["baseline", "candidate", "candidate", "baseline", "baseline", "candidate"] + assert [options["cwd"].name for _, options in e.calls] == expected_order + harness = str(e.candidate / "tests/functional/test_gateway_silent_reply_process_e2e.py") + profiles = [] + for label, (command, options) in zip(expected_order, e.calls, strict=True): + source = getattr(e, label) + assert command[0] == (str(e.baseline_python) if label == "baseline" else sys.executable) + assert command[1] == harness + assert command[-2:] == ["--source-root", str(source)] + assert options["env"]["PYTHONPATH"] == str(source / "src") + profiles.append(options["env"]["OPENSQUILLA_HOME"]) + assert len(set(profiles)) == 6 + assert e.verified == [ + (e.baseline, "a" * 40, {"archived": True}), + (e.candidate, "b" * 40, {}), + ] + assert report["dependency_mode"] == "per-source-environments" + for label in ("baseline", "candidate"): + assert report["dependency_inputs"][label] == { + name: hashlib.sha256((getattr(e, label) / name).read_bytes()).hexdigest() + for name in ("uv.lock", "pyproject.toml") + } + assert report["interpreters"][label] == { + "version": [3, 12, 14], + "implementation": "cpython", + "machine": "x86_64", + } + assert len(report["samples"][label]) == 3 + assert report["uv_lock_sha256"] == report["dependency_inputs"]["candidate"]["uv.lock"] + assert report["pyproject_sha256"] == report["dependency_inputs"]["candidate"]["pyproject.toml"] + assert report["candidate_over_baseline"] == { + "startup_to_health_ms": 2.0, + "default_first_turn_ms": 2.0, + } + assert str(e.baseline.parent) not in json.dumps(report) + + +@pytest.mark.parametrize( + "field,value", + [ + ("version", [3, 13, 14]), + ("implementation", "pypy"), + ("machine", "aarch64"), + ], +) +def test_comparison_rejects_incomparable_interpreters(comparison, field, value): + comparison.identities[str(comparison.baseline_python)][field] = value + with pytest.raises(ValueError, match=f"identical Python {field}"): + timing.compare(**comparison.arguments) + assert comparison.calls == [] + + +@pytest.mark.parametrize("path_alias", [False, True]) +def test_different_dependencies_reject_same_environment(comparison, path_alias): + prefix = Path(comparison.identities[sys.executable]["prefix"]) + if path_alias: + prefix = prefix / ".." / prefix.name + comparison.identities[str(comparison.baseline_python)]["prefix"] = str(prefix) + with pytest.raises(ValueError, match="separate Python environments"): + timing.compare(**comparison.arguments) + assert comparison.calls == [] + + +def test_separate_environment_failure_is_not_ignored(comparison, monkeypatch, capsys): + monkeypatch.setattr( + timing.subprocess, + "run", + lambda *args, **kwargs: SimpleNamespace( + returncode=1, + stdout="", + stderr="synthetic baseline launch failure", + ), + ) + with pytest.raises(RuntimeError, match="baseline timing sample 1 failed"): + timing.compare(**comparison.arguments) + assert "synthetic baseline launch failure" in capsys.readouterr().err + + +def test_interpreter_identity_uses_real_isolated_python(monkeypatch): + monkeypatch.setenv("PYTHONPATH", "unrelated-source") + identity = timing._interpreter_identity(sys.executable) + assert identity["version"] == list(sys.version_info[:3]) + assert identity["implementation"] == sys.implementation.name + assert identity["prefix"] == sys.prefix diff --git a/tests/test_inference_postprocess_rules.py b/tests/test_inference_postprocess_rules.py index bd8ddb098c..559a1a40c8 100644 --- a/tests/test_inference_postprocess_rules.py +++ b/tests/test_inference_postprocess_rules.py @@ -52,7 +52,7 @@ def test_under_routing_safety_promotes_heavy_tail_to_r2(): decision = _decide([0.40, 0.14, 0.31, 0.15]) assert decision.route_class == "R2" - assert decision.selected_model == "deepseek/deepseek-v4-pro-0813" + assert decision.selected_model == "deepseek/deepseek-v4-pro" assert decision.thinking_mode == "T2" @@ -66,7 +66,7 @@ def test_high_risk_flags_override_low_probability_route_to_r2_p2_t3(): assert decision.flags["high_risk"] is True assert decision.prompt_policy == "P2" assert decision.thinking_mode == "T3" - assert decision.selected_model == "deepseek/deepseek-v4-pro-0813" + assert decision.selected_model == "deepseek/deepseek-v4-pro" def test_trivial_ack_forces_t0_p0_on_r0(): @@ -92,7 +92,7 @@ def test_optional_sticky_tier_blocks_downgrade_without_margin_gate(): assert decision.route_class == "R3" assert decision.sticky_applied is True - assert decision.selected_model == "z-ai/glm-5.3" + assert decision.selected_model == "moonshotai/kimi-k3" def test_optional_sticky_tier_still_blocks_downgrade_after_margin_upgrade(): @@ -109,7 +109,7 @@ def test_optional_sticky_tier_still_blocks_downgrade_after_margin_upgrade(): assert decision.route_class == "R3" assert decision.sticky_applied is True - assert decision.selected_model == "z-ai/glm-5.3" + assert decision.selected_model == "moonshotai/kimi-k3" def test_aux_downgrade_can_lower_high_route_when_enabled_and_not_margin_upgraded(): @@ -125,7 +125,7 @@ def test_aux_downgrade_can_lower_high_route_when_enabled_and_not_margin_upgraded assert decision.route_class == "R2" assert decision.aux_downgrade_applied is True - assert decision.selected_model == "deepseek/deepseek-v4-pro-0813" + assert decision.selected_model == "deepseek/deepseek-v4-pro" def test_deep_conversation_context_floors_low_route_to_r1(): @@ -135,4 +135,4 @@ def test_deep_conversation_context_floors_low_route_to_r1(): ) assert decision.route_class == "R1" - assert decision.selected_model == "deepseek/deepseek-v4-flash-0731" + assert decision.selected_model == "deepseek/deepseek-v4-flash" diff --git a/tests/test_lightgbm_router_model.py b/tests/test_lightgbm_router_model.py new file mode 100644 index 0000000000..ef156da344 --- /dev/null +++ b/tests/test_lightgbm_router_model.py @@ -0,0 +1,342 @@ +"""Validate the migrated pair against frozen independent source outputs.""" + +from __future__ import annotations + +import json +import math +import platform +import shutil +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +import pytest + +from opensquilla.squilla_router.lightgbm_model import ( + MANIFEST_HASH, + MODEL_SET_ID, + LightGBMInferenceError, + LightGBMModelError, + LightGBMRouterModel, + default_model_root, + validate_artifacts, +) + + +def _copy_standalone_bundle(root: Path) -> None: + shutil.copytree(default_model_root(), root, dirs_exist_ok=True) + encoder = root / "preprocess/feature/artifacts/bge" + encoder.mkdir(parents=True, exist_ok=True) + shared = default_model_root().parents[1] / "models/v4.2_phase3_inference/bge_onnx" + for filename in ("model.onnx", "tokenizer.json"): + shutil.copyfile(shared / filename, encoder / filename) + + +@pytest.fixture(scope="module") +def model(): + pytest.importorskip("lightgbm") + pytest.importorskip("onnxruntime") + pytest.importorskip("tokenizers") + pytest.importorskip("sklearn") + with patch( + "socket.socket.connect", side_effect=AssertionError("inference must remain offline") + ): + runtime = LightGBMRouterModel() + yield runtime + runtime.close() + + +def test_manifest_and_boot_probe_do_not_import_ml(): + identity = validate_artifacts() + assert identity["model_set_id"] == MODEL_SET_ID + assert identity["model_manifest_hash"] == MANIFEST_HASH + assert identity["artifact_count"] == 18 + code = ( + "import sys; from opensquilla.squilla_router.lightgbm_model import validate_artifacts; " + "validate_artifacts(); " + "assert not {'numpy','lightgbm','onnxruntime','sklearn','router_training'} " + "& set(sys.modules)" + ) + subprocess.run([sys.executable, "-c", code], check=True) + + +def test_original_classifier_golden_vectors(model): + import numpy as np + + for target in ("intent", "tier"): + golden = json.loads( + (default_model_root() / target / "golden_vectors.json").read_text(encoding="utf-8") + ) + for case in golden["cases"]: + prediction = model.predict_features(case["features"]) + values = getattr(prediction, target + "_probabilities") + np.testing.assert_allclose( + [values[label] for label in golden["classes"]], + case["expected"]["calibrated_probabilities"], + rtol=1e-10, + atol=1e-12, + ) + expected = dict( + zip(golden["classes"], case["expected"]["calibrated_probabilities"], strict=True) + ) + labels = ("continue", "redo", "new_task") if target == "intent" else tuple(values) + assert getattr(prediction, target + "_decision") == max( + labels, key=expected.__getitem__ + ) + + +def test_runtime_identity_does_not_require_distribution_metadata(model): + from importlib.metadata import PackageNotFoundError + + with patch("importlib.metadata.version", side_effect=PackageNotFoundError("frozen package")): + runtime = LightGBMRouterModel() + try: + assert runtime.identity["runtime_versions"] == model.identity["runtime_versions"] + finally: + runtime.close() + + +def test_full_features_and_outputs_match_frozen_source(model): + import numpy as np + + from opensquilla.engine.routing.model_policy import ModelPolicyState, decide_model_tier + + # Inspect the actual native session; VNNI hosts otherwise hide U8S8 overflow. + options = model._encoder.session.get_session_options() + assert options.get_session_config_entry("session.x64quantprecision") == "1" + arm64 = platform.machine().lower() in {"arm64", "aarch64"} + if arm64: + assert options.get_session_config_entry( + "optimization.disable_specified_optimizers" + ) == "MatMulIntegerToFloatFusion;GeluFusionL2" + # Bounds apply only to measured CPU/input pairs; all labels and policy state + # still require exact agreement. Evidence lives in runtime_compatibility.json. + quantization_bounds = { + "long_three_segments": { + "feature": 0.0062 if arm64 else 0.0035, + "intent": 0.003, + "tier": 0.007, + }, + } + if arm64: + quantization_bounds["code_debug"] = { + "feature": 0.0065, "intent": 0.00065, "tier": 0.0018, + } + + reference = json.loads( + (default_model_root() / "source_parity_cases.json").read_text(encoding="utf-8") + ) + for case in reference["cases"]: + features = model.build_features(case["input"]) + assert features.dtype == np.float32 + assert features.shape == (380,) + expected_features = np.asarray(case["features"], dtype=np.float32) + bounds = quantization_bounds.get(case["name"]) + semantic = np.r_[0:64, 212:276, 292:356] + other = np.r_[64:212, 276:292, 356:380] + np.testing.assert_allclose( + features[semantic], + expected_features[semantic], + rtol=0 if bounds else 2e-5, + atol=bounds["feature"] if bounds else 2e-6, + err_msg=case["name"], + ) + np.testing.assert_allclose( + features[other], + expected_features[other], + rtol=2e-5, + atol=2e-6, + err_msg=case["name"], + ) + prediction = model.predict(case["input"]) + for target in ("intent", "tier"): + values = getattr(prediction, target + "_probabilities") + expected = case[target + "_probabilities"] + np.testing.assert_allclose( + list(values.values()), + [expected[label] for label in values], + rtol=0 if bounds else 1e-10, + atol=bounds[target] if bounds else 1e-12, + err_msg=case["name"], + ) + labels = ("continue", "redo", "new_task") if target == "intent" else tuple(values) + assert getattr(prediction, target + "_decision") == max( + labels, key=expected.__getitem__ + ), case["name"] + assert math.isclose(sum(values.values()), 1.0, abs_tol=1e-12) + # Numeric tolerance must never excuse a different route or control head. + prior = case["input"].get("active_route_tier") + state = ModelPolicyState(prior.lower(), task_id="reference-task") if prior else None + reference_prediction = SimpleNamespace( + intent_probabilities=case["intent_probabilities"], + tier_probabilities=case["tier_probabilities"], + ) + decisions = [ + decide_model_tier( + case["input"], + state, + SimpleNamespace(predict=lambda _, result=result: result), + task_id_factory=lambda: "reference-new-task", + ) + for result in (prediction, reference_prediction) + ] + for field in ( + "intent", + "tier", + "raw_tier", + "thinking_mode", + "prompt_policy", + "context_reset", + ): + assert getattr(decisions[0], field) == getattr(decisions[1], field), case["name"] + assert decisions[0].next_state == decisions[1].next_state, case["name"] + assert prediction.identity["model_manifest_hash"] == MANIFEST_HASH + # Hashes provide strict per-machine evidence even where CPU kernels vary. + assert prediction.feature_vector_hash.startswith("sha256:") + + +def test_empty_semantic_segments_are_zero_and_inputs_are_validated(model): + import numpy as np + + features = model.build_features({"current_request": "hello"}) + np.testing.assert_array_equal(features[212:276], 0.0) + np.testing.assert_array_equal(features[292:356], 0.0) + with pytest.raises(LightGBMInferenceError) as caught: + model.predict({"current_request": "hello", "surprise": "extra"}) + assert not getattr(caught.value, "fail_closed", False) + with pytest.raises(LightGBMInferenceError): + model.predict({"current_request": " "}) + + +def test_tampered_manifest_fails_closed(tmp_path: Path): + (tmp_path / "model_manifest.json").write_text("{}") + with pytest.raises(LightGBMModelError, match="manifest hash mismatch") as caught: + validate_artifacts(tmp_path) + assert caught.value.fail_closed + + +@pytest.mark.parametrize( + ("relative", "payload", "message"), + [ + ("intent/calibration.json", b"{}", "artifact hash mismatch: intent/calibration.json"), + ("preprocess/feature/artifacts/pca64.joblib", None, "assets are missing or invalid"), + ("intent/model.txt.gz", b"\x1f\x8b\x08", "assets are missing or invalid"), + ("preprocess/feature/artifacts/bge/tokenizer.json", None, "assets are missing or invalid"), + ], +) +def test_invalid_bundle_is_rejected_before_deserialization(tmp_path, relative, payload, message): + _copy_standalone_bundle(tmp_path) + path = tmp_path / relative + if payload is None: + path.unlink() + else: + path.write_bytes(payload) + with patch("opensquilla.squilla_router.lightgbm_model.model._load_transformer") as loader: + with pytest.raises(LightGBMModelError, match=message) as caught: + LightGBMRouterModel(tmp_path) + assert caught.value.fail_closed + loader.assert_not_called() + + +@pytest.mark.parametrize("filename", ["model.onnx", "tokenizer.json"]) +@pytest.mark.parametrize("missing", [False, True], ids=["tampered", "missing"]) +def test_invalid_shared_encoder_is_rejected_before_deserialization( + tmp_path, monkeypatch, filename, missing, +): + from opensquilla.squilla_router.lightgbm_model import model as module + + root = tmp_path / "lightgbm_model/assets" + _copy_standalone_bundle(root) + shared = tmp_path / "models/v4.2_phase3_inference/bge_onnx" + shared.parent.mkdir(parents=True) + (root / "preprocess/feature/artifacts/bge").rename(shared) + path = shared / filename + if missing: + path.unlink() + else: + path.write_bytes(b"invalid") + monkeypatch.setattr(module, "default_model_root", lambda: root) + message = ( + "assets are missing or invalid" if missing + else f"artifact hash mismatch: bge_onnx/{filename}" + ) + with patch.object(module, "_load_transformer") as loader: + with pytest.raises(LightGBMModelError, match=message): + LightGBMRouterModel() + loader.assert_not_called() + + +def test_native_model_loads_from_deep_install_path_with_non_utf8_locale(tmp_path, monkeypatch): + # Match the installed Gateway's long prefix while keeping resource names short. + root = tmp_path / ("installed-gateway-" + "x" * max(1, 145 - len(str(tmp_path)))) / "assets" + _copy_standalone_bundle(root) + original_read_text = Path.read_text + reads = [] + + def legacy_read_text(path, encoding=None, errors=None): + if path.is_relative_to(root): + reads.append((path.name, encoding)) + return original_read_text(path, encoding=encoding or "cp1252", errors=errors) + + monkeypatch.setattr(Path, "read_text", legacy_read_text) + runtime = LightGBMRouterModel(root) + try: + prediction = runtime.predict({"current_request": "你好,请解释一下什么是二分查找。"}) + assert prediction.tier_decision == "C0" + assert reads and all(encoding == "utf-8" for _, encoding in reads) + assert len(str(root / "preprocess/feature/artifacts/bge/model.onnx")) > 200 + finally: + runtime.close() + + +def test_encoder_cache_is_bounded_lru_and_releases_owned_rows(monkeypatch): + import numpy as np + + from opensquilla.squilla_router.lightgbm_model._vendor.features import runtime + + monkeypatch.setattr(runtime, "_BGE_MEMORY_CACHE_MAX_ENTRIES", 2) + calls = [] + + class Tokenizer: + def token_to_id(self, token): + return {"[PAD]": 0, "[CLS]": 1, "[SEP]": 2}[token] + + def encode(self, text, *, add_special_tokens): + return SimpleNamespace(ids=[int(text) + 10]) + + class Session: + def get_inputs(self): + return [SimpleNamespace(name="input_ids", shape=[1, 512])] + + def run(self, _, feed): + value = int(feed["input_ids"][0, 1]) + calls.append(value) + return [np.asarray([[value, 1, 2, 3]], dtype=np.float32)] + + encoder = runtime.BGEOnnxEncoder( + default_model_root().parents[1] / "models/v4.2_phase3_inference/bge_onnx", + tokenizer=Tokenizer(), + session=Session(), + ) + + def encode(*texts): + return encoder.encode(texts, segment_kinds=["current_request"] * len(texts)) + + first = encode("1", "2", "1") + assert calls == [11, 12] # Duplicate rows in one batch share an inference. + np.testing.assert_array_equal(first[0], first[2]) + encode("1") # Refresh the first entry before inserting a third. + encode("3") + encode("1") + assert calls == [11, 12, 13] + np.testing.assert_array_equal(encode("2")[0], first[1]) + assert calls == [11, 12, 13, 12] # The least recently used row was recomputed. + assert encode("4", "5", "6").shape == (3, 4) + assert len(encoder._memory_cache) == 2 + assert all(value.flags.owndata for value in encoder._memory_cache.values()) + encoder.close() + assert not encoder._memory_cache + with pytest.raises(RuntimeError, match="closed"): + encode("1") diff --git a/tests/test_live_long_task_case_driver.py b/tests/test_live_long_task_case_driver.py index 2c7ecc81c8..f465071781 100644 --- a/tests/test_live_long_task_case_driver.py +++ b/tests/test_live_long_task_case_driver.py @@ -368,6 +368,7 @@ def test_gateway_config_contains_env_names_but_not_credential_values( assert 'api_key_env = "TOKENRHYTHM_API_KEY"' in rendered assert "http://127.0.0.1:12345/v1" in rendered assert "enabled = true" in rendered + assert 'strategy = "legacy_v4_phase3"' in rendered @pytest.fixture diff --git a/tests/test_live_provider_profile_gateway_e2e.py b/tests/test_live_provider_profile_gateway_e2e.py index e0448fe4f8..b29a92542f 100644 --- a/tests/test_live_provider_profile_gateway_e2e.py +++ b/tests/test_live_provider_profile_gateway_e2e.py @@ -785,6 +785,7 @@ def test_attachment_capacity_config_is_single_call_with_configured_vision_c2( turn_hard_deadline_seconds=e2e.ATTACHMENT_CAPACITY_AGENT_TIMEOUT_SECONDS, llm_context_window_tokens=e2e.ATTACHMENT_CAPACITY_CONTEXT_WINDOW_TOKENS, model_supports_vision_override=str(tiers["c2"]["model"]), + router_strategy="legacy_v4_phase3", ) data = tomllib.loads(config_path.read_text(encoding="utf-8")) @@ -796,6 +797,7 @@ def test_attachment_capacity_config_is_single_call_with_configured_vision_c2( assert data["task_runtime"]["turn_hard_deadline_s"] == 75.0 assert data["naming"]["enabled"] is False assert "image_model" not in data["squilla_router"]["tiers"] + assert data["squilla_router"]["strategy"] == "legacy_v4_phase3" assert data["squilla_router"]["tiers"]["c2"] == tiers["c2"] assert str(tiers["c2"]["model"]).strip() assert tiers["c2"]["image_only"] is False diff --git a/tests/test_migration/test_remove_router_self_learning.py b/tests/test_migration/test_remove_router_self_learning.py index 5059ec4989..f7a4288a92 100644 --- a/tests/test_migration/test_remove_router_self_learning.py +++ b/tests/test_migration/test_remove_router_self_learning.py @@ -203,7 +203,9 @@ def test_retired_environment_values_are_not_validated(monkeypatch): def test_explicit_base_model_path_is_used_even_when_named_learned(tmp_path, monkeypatch): path = tmp_path / "router" / "learned" / "explicit-base" - cfg = GatewayConfig(squilla_router={"v4_bundle_dir": str(path)}) + cfg = GatewayConfig(squilla_router={ + "strategy": "legacy_v4_phase3", "v4_bundle_dir": str(path), + }) constructor = Mock() monkeypatch.setattr("opensquilla.squilla_router.v4_phase3.V4Phase3Strategy", constructor) monkeypatch.setattr(router_step, "_strategy", None) @@ -244,6 +246,8 @@ async def test_first_route_never_accesses_historical_learning_state( monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(home)) config_path = home / "config.toml" payload = _legacy_payload() + # This fixture exercises the retired v4 factory and its historical-state isolation. + payload["squilla_router"]["strategy"] = "legacy_v4_phase3" payload["squilla_router"].pop("v4_bundle_dir") _write(config_path, payload) diff --git a/tests/test_model_router_behavior.py b/tests/test_model_router_behavior.py index 069b104389..198cb84e5c 100644 --- a/tests/test_model_router_behavior.py +++ b/tests/test_model_router_behavior.py @@ -102,9 +102,18 @@ def make_context( raw_message: str | None = None, attachments: list[dict] | None = None, ) -> TurnContext: - # Pin the packaged OpenRouter ladder so tier transitions can be asserted - # against its distinct per-tier models. - config = GatewayConfig(llm={"provider": "openrouter"}) + # These classifier/rule tests exercise the explicitly retained legacy mode. + # Pin its historical deployments rather than depending on the new model pool. + config = GatewayConfig( + llm={"provider": "openrouter"}, + squilla_router={"strategy": "legacy_v4_phase3"}, + ) + for tier, model in { + "c1": "deepseek/deepseek-v4-flash-0731", + "c2": "deepseek/deepseek-v4-pro-0813", + "c3": "z-ai/glm-5.3", + }.items(): + config.squilla_router.tiers[tier]["model"] = model config.squilla_router.rollout_phase = rollout_phase return TurnContext( message=message, @@ -1496,7 +1505,7 @@ async def test_tokenrhythm_default_image_route_uses_configured_catalog_vision( "_get_strategy", lambda _config: pytest.fail("image routing should not invoke text strategy"), ) - config = GatewayConfig() + config = GatewayConfig(squilla_router={"strategy": "legacy_v4_phase3"}) assert config.squilla_router.tiers["c0"]["model"] == "qwen3.7-flash" config.squilla_router.tiers["c0"]["supports_image"] = False ctx = TurnContext( @@ -1518,8 +1527,8 @@ async def test_tokenrhythm_default_image_route_uses_configured_catalog_vision( assert routed.metadata["routed_model_vision_support"] == "supported" assert routed.metadata["router_fallback_chain"] == [ { - "tier": "c1", - "model": "deepseek-flash", + "tier": "c3", + "model": "kimi-k3", "vision_support": "supported", "provider": "tokenrhythm", } diff --git a/tests/test_model_router_defaults.py b/tests/test_model_router_defaults.py index 1cb70081c8..c5e5c6f566 100644 --- a/tests/test_model_router_defaults.py +++ b/tests/test_model_router_defaults.py @@ -43,7 +43,8 @@ def test_squilla_router_defaults_match_runtime_router_config() -> None: assert cfg.enabled is True assert cfg.auto_thinking is True assert cfg.rollout_phase == "full" - assert cfg.strategy == "v4_phase3" + assert cfg.strategy == "lightgbm_model" + assert cfg.tier_policy == "model" assert cfg.default_tier == "c1" assert cfg.confidence_threshold == 0.5 assert cfg.confidence_high_tier_margin == 0.05 @@ -58,11 +59,11 @@ def test_squilla_router_defaults_match_runtime_router_config() -> None: assert cfg.tiers["c0"]["model"] == "qwen/qwen3.7-flash" assert cfg.tiers["c0"]["thinking_level"] == "high" - assert cfg.tiers["c1"]["model"] == "deepseek/deepseek-v4-flash-0731" + assert cfg.tiers["c1"]["model"] == "deepseek/deepseek-v4-flash" assert cfg.tiers["c1"]["thinking_level"] == "high" - assert cfg.tiers["c2"]["model"] == "deepseek/deepseek-v4-pro-0813" + assert cfg.tiers["c2"]["model"] == "deepseek/deepseek-v4-pro" assert cfg.tiers["c2"]["thinking_level"] == "high" - assert cfg.tiers["c3"]["model"] == "z-ai/glm-5.3" + assert cfg.tiers["c3"]["model"] == "moonshotai/kimi-k3" assert cfg.tiers["c3"]["thinking_level"] == "high" assert cfg.tiers["image_model"]["model"] == "moonshotai/kimi-k2.6" assert cfg.tiers["image_model"]["supports_image"] is True @@ -198,9 +199,9 @@ def test_direct_legacy_openrouter_router_defaults_are_migrated(provider_id: str) TOKENRHYTHM_EXPECTED_TIER_MODELS = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", } @@ -302,9 +303,7 @@ def test_each_provider_profile_has_four_text_tiers_without_default_image_model() cfg = squilla_router_config_cls(tier_profile=profile) assert {"c0", "c1", "c2", "c3"}.issubset(cfg.tiers) assert "image_model" not in cfg.tiers - assert {cfg.tiers[tier]["provider"] for tier in ("c0", "c1", "c2", "c3")} == { - profile - } + assert {cfg.tiers[tier]["provider"] for tier in ("c0", "c1", "c2", "c3")} == {profile} def test_direct_provider_profiles_have_four_text_tiers_without_default_image_model() -> None: @@ -314,9 +313,7 @@ def test_direct_provider_profiles_have_four_text_tiers_without_default_image_mod cfg = squilla_router_config_cls(tier_profile=profile) assert {"c0", "c1", "c2", "c3"}.issubset(cfg.tiers) assert "image_model" not in cfg.tiers - assert {cfg.tiers[tier]["provider"] for tier in ("c0", "c1", "c2", "c3")} == { - profile - } + assert {cfg.tiers[tier]["provider"] for tier in ("c0", "c1", "c2", "c3")} == {profile} def test_openai_profile_uses_streaming_compatible_models() -> None: @@ -356,9 +353,7 @@ def test_moonshot_profile_uses_kimi_for_strong_tiers() -> None: assert cfg.tiers["c1"]["model"] == "kimi-k2.6" assert cfg.tiers["c2"]["model"] == "kimi-k2.6" assert cfg.tiers["c3"]["model"] == "kimi-k2.7-code" - assert all( - cfg.tiers[tier]["supports_image"] is True for tier in ("c0", "c1", "c2", "c3") - ) + assert all(cfg.tiers[tier]["supports_image"] is True for tier in ("c0", "c1", "c2", "c3")) def test_volcengine_profile_uses_seed_2_capability_ladder() -> None: @@ -449,7 +444,8 @@ def test_example_toml_enables_runtime_router_defaults() -> None: assert squilla_router["enabled"] is True assert squilla_router["auto_thinking"] is True assert squilla_router["rollout_phase"] == "full" - assert squilla_router["strategy"] == "v4_phase3" + assert squilla_router["strategy"] == "lightgbm_model" + assert squilla_router["tier_policy"] == "model" assert "cache_ttl_seconds" not in squilla_router assert squilla_router["default_tier"] == "c1" assert squilla_router["confidence_threshold"] == 0.5 @@ -469,10 +465,10 @@ def test_example_toml_enables_runtime_router_defaults() -> None: assert "thinking_level" not in tiers[name] assert tiers["c0"]["model"] == "qwen3.7-flash" assert tiers["c0"]["supports_image"] is False - assert tiers["c1"]["model"] == "deepseek-flash" + assert tiers["c1"]["model"] == "deepseek-v4-flash-0731" assert tiers["c2"]["model"] == "deepseek-v4-pro-0813" assert tiers["c2"]["supports_image"] is False - assert tiers["c3"]["model"] == "glm-5.3" + assert tiers["c3"]["model"] == "kimi-k3" assert tiers["c3"]["ensemble_enabled"] is False assert "ensemble_selection_mode" not in tiers["c3"] assert tiers["image_model"]["model"] == "kimi-k2.6" @@ -495,12 +491,18 @@ def test_runtime_router_config_does_not_ship_unused_cost_fields() -> None: data = yaml.safe_load(text) assert data["tier_registry"]["S"] == ["qwen/qwen3.7-flash"] - assert data["tier_registry"]["M"] == ["deepseek/deepseek-v4-flash-0731"] - assert data["tier_registry"]["L"] == ["deepseek/deepseek-v4-pro-0813"] - assert data["tier_registry"]["XL"] == ["z-ai/glm-5.3"] - assert data["tier_explanations"]["L"]["model"] == "deepseek/deepseek-v4-pro-0813" - assert data["tier_explanations"]["XL"]["model"] == "z-ai/glm-5.3" + assert data["tier_registry"]["M"] == ["deepseek/deepseek-v4-flash"] + assert data["tier_registry"]["L"] == ["deepseek/deepseek-v4-pro"] + assert data["tier_registry"]["XL"] == ["moonshotai/kimi-k3"] + assert data["tier_explanations"]["L"]["model"] == "deepseek/deepseek-v4-pro" + assert data["tier_explanations"]["XL"]["model"] == "moonshotai/kimi-k3" assert "cost_ratios:" not in text assert "cost_matrix:" not in text assert "under_routing_multiplier" not in text assert "over_routing_multiplier" not in text + + +def test_existing_strategy_settings_upgrade_but_explicit_legacy_remains_available(): + cls = _squilla_router_config_cls() + assert cls(strategy="v4_phase3").strategy == "lightgbm_model" + assert cls(strategy="legacy_v4_phase3").strategy == "legacy_v4_phase3" diff --git a/tests/test_model_router_step.py b/tests/test_model_router_step.py new file mode 100644 index 0000000000..06009528bc --- /dev/null +++ b/tests/test_model_router_step.py @@ -0,0 +1,401 @@ +from __future__ import annotations + +from copy import deepcopy +from types import SimpleNamespace + +import pytest + +from opensquilla.engine.pipeline import TurnContext +from opensquilla.engine.routing.model_policy import ModelPolicyState +from opensquilla.engine.steps import squilla_router +from opensquilla.engine.steps.model_router import ModelRouteError, validate_model_route_capacity +from opensquilla.gateway.config import GatewayConfig, SquillaRouterConfig +from opensquilla.router_control import RouterControlHoldStore + + +class Classifier: + def __init__(self, intent="new_task", tier="c0"): + self.intent, self.tier = intent, tier + self.inputs = [] + + def predict(self, value): + self.inputs.append(deepcopy(value)) + return SimpleNamespace( + intent_probabilities={ + k: float(k == self.intent) for k in ("new_task", "continue", "redo") + }, + tier_probabilities={k: float(k == self.tier) for k in ("c0", "c1", "c2", "c3")}, + intent_decision=self.intent, + tier_decision=self.tier, + ) + + +def context(*, message="Explain this.", state=None, provider="openrouter", **router): + cfg = GatewayConfig(llm={"provider": provider}, squilla_router=router) + ctx = TurnContext( + message=message, + session_key="model-test", + config=cfg, + provider=None, + model=cfg.llm.model, + tool_defs=[], + system_prompt="", + ) + ctx.metadata["router_model_input_message_id"] = "input-2" + if state: + ctx.metadata["router_model_state"] = state.to_dict() + return ctx + + +def install(monkeypatch, classifier): + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: classifier) + + +def test_current_and_saved_v4_defaults_use_model_policy(): + assert SquillaRouterConfig().strategy == "lightgbm_model" + assert SquillaRouterConfig(strategy="v4_phase3").strategy == "lightgbm_model" + assert SquillaRouterConfig().tier_policy == "model" + assert squilla_router._strategy_name(SquillaRouterConfig()) == "lightgbm_model" + assert ( + squilla_router._strategy_name(SquillaRouterConfig(strategy="legacy_v4_phase3")) + == "v4_phase3" + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "intent,expected", [("continue", "c1"), ("redo", "c2"), ("new_task", "c0")] +) +async def test_model_policy_bypasses_legacy_complaint_confidence_and_context_rules( + monkeypatch, intent, expected +): + model = Classifier(intent, "c0") + install(monkeypatch, model) + ctx = context( + message="Your calculation is wrong. Debug production security failure. " + "data " * 3000, + state=ModelPolicyState("c1"), + confidence_threshold=1.0, + budget={"action": "cap", "cap_tier": "c0", "limit_usd": 0.01}, + ) + ctx.metadata.update(session_billed_cost_usd=1.0, session_cost_source="billed") + result = await squilla_router.apply_squilla_router(ctx) + assert result.metadata["routed_tier"] == expected + assert result.model == result.config.squilla_router.tiers[expected]["model"] + assert result.metadata["router_fallback_chain"] == [] + assert result.metadata["router_model_decision"]["state"]["tier"] == expected + assert result.metadata["routing_extra"]["tier_policy"] == "model" + assert len(model.inputs) == 1 + assert ( + model.inputs[0]["current_request"] == ctx.semantic_message + or result.metadata["prompt_policy"] == "P0" + ) + + +@pytest.mark.asyncio +async def test_replay_keeps_same_decision_without_predicting_or_redo_twice(monkeypatch): + model = Classifier("redo") + install(monkeypatch, model) + first = await squilla_router.apply_squilla_router(context(state=ModelPolicyState("c1"))) + replay = context( + state=ModelPolicyState.from_dict(first.metadata["router_model_decision"]["state"]) + ) + replay.metadata["router_model_decision"] = deepcopy(first.metadata["router_model_decision"]) + monkeypatch.setattr( + squilla_router, "_get_strategy", lambda _: pytest.fail("replay must not classify") + ) + second = await squilla_router.apply_squilla_router(replay) + assert second.metadata["routed_tier"] == "c2" + assert ( + second.metadata["router_model_decision"]["state"] + == first.metadata["router_model_decision"]["state"] + ) + + +@pytest.mark.asyncio +async def test_observation_does_not_commit_candidate_state(monkeypatch): + install(monkeypatch, Classifier("redo")) + ctx = context(state=ModelPolicyState("c1"), rollout_phase="observe") + original = ctx.model + await squilla_router.apply_squilla_router(ctx) + assert ctx.model == original + assert "router_model_decision" not in ctx.metadata + assert ctx.metadata["routing_applied"] is False + assert ctx.metadata["routed_tier"] == "c2" + assert "router_fallback_strict" not in ctx.metadata + assert "router_fallback_chain" not in ctx.metadata + assert "thinking_requested" not in ctx.metadata + + +@pytest.mark.asyncio +async def test_model_manual_hold_overrides_after_intent_prediction(monkeypatch): + model = Classifier("redo") + install(monkeypatch, model) + ctx = context(state=ModelPolicyState("c1")) + store = RouterControlHoldStore() + target = next( + t for t in store.build_targets(ctx.config.squilla_router) if t.target_id == "tier:c0" + ) + store.set_hold(ctx.session_key, target, evidence="use c0") + ctx.metadata["router_control_hold_store"] = store + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["routed_tier"] == "c0" + assert ctx.metadata["router_model_decision"]["state"]["tier"] == "c0" + assert ctx.metadata["router_control_hold_applied"] is True + assert len(model.inputs) == 1 + + +@pytest.mark.asyncio +async def test_image_turn_uses_model_then_validates_selected_deployment(monkeypatch): + model = Classifier(tier="c2") + install(monkeypatch, model) + ctx = context() + ctx.attachments = [{"type": "image/png"}] + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["routed_tier"] == "c2" + assert len(model.inputs) == 1 + monkeypatch.setattr(squilla_router, "_tier_deployment_vision_support", lambda *_: "unsupported") + with pytest.raises(ModelRouteError, match="image support"): + validate_model_route_capacity(ctx) + assert ctx.metadata["routed_tier"] == "c2" + + +@pytest.mark.asyncio +async def test_unconfigured_prediction_is_not_replaced_by_another_tier(monkeypatch): + install(monkeypatch, Classifier(tier="c3")) + ctx = context() + ctx.config.squilla_router.tiers.pop("c3") + with pytest.raises(ModelRouteError, match="unconfigured tier c3"): + await squilla_router.apply_squilla_router(ctx) + + +@pytest.mark.asyncio +async def test_t0_explicitly_disables_provider_thinking(monkeypatch): + install(monkeypatch, Classifier(tier="c0")) + ctx = context(message="Hello") + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["thinking_mode"] == "T0" + assert ctx.metadata["thinking_requested"] is True + assert ctx.metadata["thinking_level"] == "off" + + +@pytest.mark.asyncio +async def test_generation_context_controls_effort_without_changing_tier(monkeypatch): + install(monkeypatch, Classifier("continue")) + ctx = context(state=ModelPolicyState("c2")) + ctx.metadata["router_model_control_context"] = {"context_tokens_est": 3000} + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["routed_tier"] == "c2" + assert ctx.metadata["thinking_mode"] == "T3" + assert ctx.metadata["thinking_level"] == "high" + + +@pytest.mark.asyncio +async def test_explicit_new_disables_provider_native_context_replay(monkeypatch): + install(monkeypatch, Classifier()) + ctx = context(state=ModelPolicyState("c2")) + ctx.metadata["router_model_control_event"] = "new_task" + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["router_model_decision"]["decision"]["context_reset"] is True + assert ctx.metadata["provider_state_replay_disabled"] == "router_explicit_new_task" + + +@pytest.mark.asyncio +async def test_quality_retry_replay_uses_effective_tier_preserves_original_audit(monkeypatch): + from dataclasses import replace + + install(monkeypatch, Classifier(tier="c1")) + first = await squilla_router.apply_squilla_router(context()) + record = deepcopy(first.metadata["router_model_decision"]) + original = deepcopy(record["decision"]) + state = ModelPolicyState.from_dict(record["state"]) + record["state"] = replace(state, tier="c2", version=state.version + 1).to_dict() + record["quality_retry"] = {"used": True, "from_tier": "c1", "to_tier": "c2"} + target = first.config.squilla_router.tiers["c2"] + record["execution_update"] = { + "tier": "c2", + "model": target["model"], + "provider": target["provider"], + "thinking_mode": "T3", + "prompt_policy": "P2", + "thinking_level": "high", + } + ctx = context() + ctx.metadata["router_model_decision"] = record + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: pytest.fail("reclassified")) + await squilla_router.apply_squilla_router(ctx) + assert ctx.metadata["routed_tier"] == "c2" + assert ctx.metadata["router_model_decision"]["state"] == record["state"] + assert ctx.metadata["router_model_decision"]["decision"] == original + assert ctx.metadata["thinking_level"] == "high" + + +@pytest.mark.asyncio +async def test_nonattachment_oversize_is_rejected_without_tier_replacement(monkeypatch): + install(monkeypatch, Classifier(tier="c0")) + ctx = context() + await squilla_router.apply_squilla_router(ctx) + monkeypatch.setattr( + squilla_router, + "_capacity_tier_assessments", + lambda *a, **k: [ + SimpleNamespace(assessment=SimpleNamespace(fits=False)), + ], + ) + with pytest.raises(ModelRouteError, match="complete request"): + validate_model_route_capacity(ctx) + assert ctx.metadata["routed_tier"] == "c0" + + +@pytest.mark.parametrize("provider", ["openrouter", "tokenrhythm"]) +@pytest.mark.parametrize("tier", ["c0", "c1", "c2", "c3"]) +def test_reference_pool_is_admissible_offline(tier, provider): + ctx = context(provider=provider) + ctx.config.llm.provider = provider + ctx.metadata.update(routing_applied=True, routed_tier=tier, thinking_mode="T2") + assert validate_model_route_capacity(ctx) is ctx + + +@pytest.mark.parametrize("tier", ["c0", "c3"]) +def test_capable_selected_deployment_accepts_images_without_image_tier_shortcut(tier): + ctx = context() + ctx.attachments = [{"mime_type": "image/png"}] + ctx.metadata.update(routing_applied=True, routed_tier=tier, thinking_mode="T2") + assert validate_model_route_capacity(ctx) is ctx + assert ctx.metadata["routed_tier"] == tier + + +@pytest.mark.asyncio +async def test_default_step_runs_pinned_pair_and_reuses_durable_redo(monkeypatch): + import json + + from opensquilla.squilla_router.lightgbm_model import default_model_root + + fixture = json.loads( + (default_model_root() / "source_parity_cases.json").read_text(encoding="utf-8") + )["cases"][0] + ctx = context(message=fixture["input"]["current_request"]) + ctx.metadata["router_model_input"] = fixture["input"] + model = squilla_router.preload_strategy(ctx.config.squilla_router) + predict = model.predict + evidence = {} + + def capture_prediction(value): + prediction = predict(value) + evidence.update( + input=value, + feature_hash=prediction.feature_vector_hash, + identity=prediction.identity, + features=model.build_features(value).tolist(), + ) + return prediction + + monkeypatch.setattr(model, "predict", capture_prediction) + await squilla_router.apply_squilla_router(ctx) + original = ctx.metadata["router_model_decision"]["decision"] + assert original["tier_audit"]["probabilities"] == pytest.approx( + {k.lower(): v for k, v in fixture["tier_probabilities"].items()}, + abs=1e-10, + ), evidence + assert ctx.metadata["routed_tier"] == "c0" + status = squilla_router.router_runtime_status() + assert status["loaded"] is True + assert status["strategy"] == "lightgbm_model" + validate_model_route_capacity(ctx) + + retry = context( + message=ctx.semantic_message, + state=ModelPolicyState.from_dict( + ctx.metadata["router_model_decision"]["state"], + ), + ) + retry.metadata["router_model_control_event"] = "redo" + # Explicit redo must reuse state instead of the classifier output. + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: Classifier("new_task", "c3")) + await squilla_router.apply_squilla_router(retry) + assert retry.metadata["routed_tier"] == "c1" + assert ( + retry.metadata["router_model_decision"]["decision"]["tier_audit"]["run_status"] == "not_run" + ) + + +def test_declared_provider_cannot_silently_execute_on_primary_credentials(): + ctx = context() + ctx.config.llm.provider = "tokenrhythm" + ctx.metadata.update(routing_applied=True, routed_tier="c0", thinking_mode="T2") + with pytest.raises(ModelRouteError, match="cross-provider"): + validate_model_route_capacity(ctx) + + +def test_only_proven_history_pressure_can_defer_to_same_tier_compaction(): + ctx = context() + ctx.config.llm.context_window_tokens = 10000 + ctx.metadata.update( + routing_applied=True, + routed_tier="c0", + thinking_mode="T0", + routing_history_capacity_estimated_tokens=20000, + routing_history_capacity_message_count=20, + routing_history_capacity_estimate_complete=True, + ) + with pytest.raises(ModelRouteError, match="complete request"): + validate_model_route_capacity(ctx) + validate_model_route_capacity(ctx, allow_compaction_retry=True) + assert ctx.metadata["router_model_capacity_retry_pending"] is True + assert ctx.metadata["routed_tier"] == "c0" + # A pending marker is not authorization to execute an oversized request. + with pytest.raises(ModelRouteError, match="complete request"): + validate_model_route_capacity(ctx) + ctx.metadata["routing_history_capacity_estimated_tokens"] = 2000 + validate_model_route_capacity(ctx) + assert "router_model_capacity_retry_pending" not in ctx.metadata + + +@pytest.mark.asyncio +async def test_staged_unadmitted_decision_cannot_reclassify_or_execute(monkeypatch): + install(monkeypatch, Classifier(tier="c2")) + first = await squilla_router.apply_squilla_router(context()) + envelope = deepcopy(first.metadata["router_model_decision"]) + envelope.update(status="staged", state_committed=False) + ctx = context() + ctx.metadata["router_model_decision"] = envelope + monkeypatch.setattr(squilla_router, "_get_strategy", lambda _: pytest.fail("reclassified")) + with pytest.raises(ModelRouteError, match="not admitted"): + await squilla_router.apply_squilla_router(ctx) + assert "routing_applied" not in ctx.metadata + + +def test_model_strategy_cache_ignores_legacy_settings_but_tracks_assets(monkeypatch): + from opensquilla.squilla_router import lightgbm_model + + loaded = [] + + def load(model_root): + loaded.append(model_root) + return SimpleNamespace(source="lightgbm_model", _available=True) + + monkeypatch.setattr(lightgbm_model, "LightGBMRouterModel", load) + monkeypatch.setattr(squilla_router, "_strategy", None) + monkeypatch.setattr(squilla_router, "_strategy_key", None) + config = SquillaRouterConfig() + first = squilla_router.preload_strategy(config) + config.confidence_threshold = 0.99 + config.v4_bundle_dir = "/unused-legacy-bundle" + config.v4_use_aux_head = True + config.require_router_runtime = True + assert squilla_router.preload_strategy(config) is first + assert loaded == [None] + status = squilla_router.router_runtime_status() + assert status["loaded"] is True + assert status["strategy"] == "lightgbm_model" + config.lightgbm_model_dir = "/another-model-bundle" + assert squilla_router.preload_strategy(config) is not first + assert loaded == [None, "/another-model-bundle"] + + +def test_explicit_thinking_requires_reasoning_even_when_policy_suggests_t0(): + ctx = context(auto_thinking=False) + ctx.config.llm.thinking = "high" + ctx.config.squilla_router.tiers["c0"]["model"] = "qwen/qwen-vl-max" + ctx.metadata.update(routing_applied=True, routed_tier="c0", thinking_mode="T0") + with pytest.raises(ModelRouteError, match="does not support reasoning"): + validate_model_route_capacity(ctx) diff --git a/tests/test_onboarding/test_flow.py b/tests/test_onboarding/test_flow.py index fae03f9035..e7fc20f34c 100644 --- a/tests/test_onboarding/test_flow.py +++ b/tests/test_onboarding/test_flow.py @@ -1447,7 +1447,7 @@ def text(self, message: str, **kwargs): assert kwargs.get("default") == "openrouter" return _Answer("openrouter") if message == "c2 model": - assert kwargs.get("default") == "deepseek/deepseek-v4-pro-0813" + assert kwargs.get("default") == "deepseek/deepseek-v4-pro" return _Answer("custom/reasoner") raise AssertionError(f"unexpected text prompt: {message}") diff --git a/tests/test_onboarding/test_mutations.py b/tests/test_onboarding/test_mutations.py index 1393622e14..ada96e4e2e 100644 --- a/tests/test_onboarding/test_mutations.py +++ b/tests/test_onboarding/test_mutations.py @@ -507,9 +507,9 @@ def test_tokenrhythm_provider_save_seeds_curated_inline_ladder(): assert res.config.squilla_router.tier_profile is None expected = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", "image_model": "kimi-k2.6", } for tier, model in expected.items(): @@ -517,7 +517,7 @@ def test_tokenrhythm_provider_save_seeds_curated_inline_ladder(): assert res.config.squilla_router.tiers[tier]["model"] == model persisted = res.config.to_toml_dict()["squilla_router"] assert "tier_profile" not in persisted - assert persisted["tiers"]["c3"]["model"] == "glm-5.3" + assert persisted["tiers"]["c3"]["model"] == "kimi-k3" assert persisted["tiers"]["c3"]["ensemble_enabled"] is False assert persisted["tiers"]["c0"]["supports_image"] is False assert persisted["tiers"]["c2"]["supports_image"] is False @@ -542,7 +542,7 @@ def test_provider_default_direct_model_does_not_follow_existing_router_tier(): assert res.config.llm.model == "deepseek/deepseek-v4-pro" assert res.config.squilla_router.default_tier == "c2" - assert res.config.squilla_router.tiers["c2"]["model"] == "deepseek/deepseek-v4-pro-0813" + assert res.config.squilla_router.tiers["c2"]["model"] == "deepseek/deepseek-v4-pro" def test_upsert_channel_appends_new(): diff --git a/tests/test_onboarding/test_provider_specs.py b/tests/test_onboarding/test_provider_specs.py index b5ee958d69..c52bdc5c30 100644 --- a/tests/test_onboarding/test_provider_specs.py +++ b/tests/test_onboarding/test_provider_specs.py @@ -99,7 +99,7 @@ def test_tokenrhythm_preset_wire_exposes_single_model_c3_default(): c3 = row["presets"][0]["tiers"]["c3"] assert c3["ensembleEnabled"] is False - assert c3["model"] == "glm-5.3" + assert c3["model"] == "kimi-k3" def test_tier_payload_preserves_a_legacy_ensemble_selection_mode(): diff --git a/tests/test_packaging/test_lightgbm_checkout_integrity.py b/tests/test_packaging/test_lightgbm_checkout_integrity.py new file mode 100644 index 0000000000..b57d54ee48 --- /dev/null +++ b/tests/test_packaging/test_lightgbm_checkout_integrity.py @@ -0,0 +1,53 @@ +"""A Windows-style checkout must retain the frozen model's byte identities.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + + +def test_autocrlf_checkout_preserves_frozen_router_text(tmp_path: Path) -> None: + if shutil.which("git") is None: + pytest.skip("Git is required to exercise checkout conversion") + root = Path(__file__).resolve().parents[2] + package = root / "src/opensquilla/squilla_router/lightgbm_model" + paths = [ + path + for path in package.rglob("*") + if path.is_file() and path.suffix in {".json", ".py", ".toml", ".md"} + ] + assert paths + repository = tmp_path / "repository" + repository.mkdir() + environment = {**os.environ, "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": os.devnull} + + def git(*arguments: str) -> None: + subprocess.run( + ["git", *arguments], cwd=repository, env=environment, check=True, capture_output=True + ) + + git("init", "--quiet") + git("config", "core.autocrlf", "true") + shutil.copyfile(root / ".gitattributes", repository / ".gitattributes") + expected = {} + for path in paths: + relative = path.relative_to(root) + destination = repository / relative + destination.parent.mkdir(parents=True, exist_ok=True) + payload = path.read_bytes() + destination.write_bytes(payload) + expected[relative] = payload + git("add", ".") + checkout = tmp_path / "checkout" + checkout.mkdir() + git("checkout-index", "--all", f"--prefix={checkout}{os.sep}") + changed = [ + str(path) + for path, original in expected.items() + if (checkout / path).read_bytes() != original + ] + assert changed == [], f"Checkout changed frozen source/model bytes: {changed}" diff --git a/tests/test_packaging/test_router_presets_packaged.py b/tests/test_packaging/test_router_presets_packaged.py index bbbd7ffe4b..316ac3c4dd 100644 --- a/tests/test_packaging/test_router_presets_packaged.py +++ b/tests/test_packaging/test_router_presets_packaged.py @@ -56,3 +56,68 @@ def test_source_tree_ships_all_packaged_router_presets() -> None: presets_dir = REPO_ROOT / "src" / "opensquilla" / "provider" / "presets" present = {p.stem for p in presets_dir.glob("*.toml")} assert present == set(PACKAGED_PRESET_IDS), present + + +def test_wheel_contains_complete_verified_lightgbm_bundle(isolated_core_wheel: Path) -> None: + import gzip + import hashlib + import json + + prefix = "opensquilla/squilla_router/lightgbm_model" + with zipfile.ZipFile(isolated_core_wheel) as wheel: + names = set(wheel.namelist()) + assert { + f"{prefix}/model.py", + f"{prefix}/input_contract.py", + f"{prefix}/calibration.py", + f"{prefix}/_vendor/features/runtime.py", + f"{prefix}/_vendor/features/numeric.py", + f"{prefix}/PROVENANCE.md", + } <= names + asset_prefix = f"{prefix}/assets" + manifest = json.loads(wheel.read(f"{asset_prefix}/artifact_manifest.json")) + from pathlib import PureWindowsPath + + # The real NSIS Gateway prefix previously made hashed encoder paths 289 + # characters and the smoke probe failed to read them. Keep the packaged + # layout below MAX_PATH without relying on installer or runtime opt-in. + installed = ( + PureWindowsPath( + "D:/a/opensquilla/opensquilla/dist/desktop-electron/win-unpacked/resources" + "/runtime/gateway/opensquilla-gateway/_internal" + ) + / asset_prefix + ) + assert not any( + name.startswith(f"{asset_prefix}/preprocess/feature/artifacts/bge/") for name in names + ) + assert all(len(str(installed / record["path"])) < 260 for record in manifest["files"]) + for record in manifest["files"]: + path = f"{asset_prefix}/{record['path']}" + assert path in names, f"missing model artifact: {path}" + payload = wheel.read(path) + assert len(payload) == record["size_bytes"], path + assert hashlib.sha256(payload).hexdigest() == record["sha256"], path + original = wheel.read(f"{asset_prefix}/model_manifest.json") + assert hashlib.sha256(original).hexdigest() == ( + "29fb79337458c4f35d3e0d1148fbdb1175c20069a5fa8ee6441633b0e2eee357" + ) + source = json.loads(original) + shared = "opensquilla/squilla_router/models/v4.2_phase3_inference/bge_onnx" + for role, filename in (("onnx_model", "model.onnx"), ("tokenizer", "tokenizer.json")): + reference = next(ref for ref in source["preprocess"] if f"/bge/{role}/" in ref["path"]) + path = f"{shared}/{filename}" + payload = wheel.read(path) + shared_installed = installed.parents[1] / "models/v4.2_phase3_inference/bge_onnx" + assert len(str(shared_installed / filename)) < 260 + assert len(payload) == reference["size"], path + assert "sha256:" + hashlib.sha256(payload).hexdigest() == reference["sha256"], path + for target in ("intent", "tier"): + reference = next( + ref + for ref in source["models"] + if (ref["artifact_type"] == "lightgbm_model" and f"/{target}/" in ref["path"]) + ) + decoded = gzip.decompress(wheel.read(f"{asset_prefix}/{target}/model.txt.gz")) + assert len(decoded) == reference["size"] + assert "sha256:" + hashlib.sha256(decoded).hexdigest() == reference["sha256"] diff --git a/tests/test_provider/golden/router_tier_profiles.json b/tests/test_provider/golden/router_tier_profiles.json index ab2d746b68..8457e370ce 100644 --- a/tests/test_provider/golden/router_tier_profiles.json +++ b/tests/test_provider/golden/router_tier_profiles.json @@ -183,22 +183,22 @@ "thinking_level": "high" }, "c1": { - "description": "Default DeepSeek V4 Flash 0731 route for normal agent work, coding assistance, debugging, and moderate analysis", - "model": "deepseek/deepseek-v4-flash-0731", + "description": "Default DeepSeek V4 Flash route for normal agent work, coding assistance, debugging, and moderate analysis", + "model": "deepseek/deepseek-v4-flash", "provider": "openrouter", "supports_image": false, "thinking_level": "high" }, "c2": { - "description": "Stronger DeepSeek V4 Pro 0813 route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis", - "model": "deepseek/deepseek-v4-pro-0813", + "description": "Stronger DeepSeek V4 Pro route for multi-step coding, structured reasoning, larger context synthesis, and harder analysis", + "model": "deepseek/deepseek-v4-pro", "provider": "openrouter", "supports_image": false, "thinking_level": "high" }, "c3": { - "description": "Highest-tier GLM 5.3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", - "model": "z-ai/glm-5.3", + "description": "Highest-tier Kimi K3 route for difficult planning, deep review, complex debugging, and high-stakes synthesis", + "model": "moonshotai/kimi-k3", "provider": "openrouter", "supports_image": false, "thinking_level": "high" diff --git a/tests/test_provider/test_preset_registry.py b/tests/test_provider/test_preset_registry.py index 8b733d7bfc..2fa43bf2e1 100644 --- a/tests/test_provider/test_preset_registry.py +++ b/tests/test_provider/test_preset_registry.py @@ -158,9 +158,9 @@ def test_tokenrhythm_curated_ladder() -> None: ) expected_models = { "c0": "qwen3.7-flash", - "c1": "deepseek-flash", + "c1": "deepseek-v4-flash-0731", "c2": "deepseek-v4-pro-0813", - "c3": "glm-5.3", + "c3": "kimi-k3", "image_model": "kimi-k2.6", } tiers = preset.tier_defaults() diff --git a/tests/test_provider_model_catalog.py b/tests/test_provider_model_catalog.py index 921e26ab35..06e928b107 100644 --- a/tests/test_provider_model_catalog.py +++ b/tests/test_provider_model_catalog.py @@ -141,10 +141,10 @@ def test_openrouter_public_physical_limits_are_available_offline( @pytest.mark.parametrize( ("provider", "expected_limits"), [ - ("openrouter", [(1_000_000, 65_536), (1_048_576, 943_718), - (1_024_000, 384_000), (1_048_576, 131_072)]), + ("openrouter", [(1_000_000, 65_536), (1_024_000, 384_000), + (1_048_576, 393_216), (1_048_576, 943_718)]), ("tokenrhythm", [(1_000_000, 65_536), (1_000_000, 384_000), - (1_000_000, 384_000), (1_048_576, 131_072)]), + (1_000_000, 384_000), (1_048_576, 1_048_576)]), ], ) def test_recommended_router_tiers_have_known_offline_capacity_and_vision( @@ -161,7 +161,7 @@ def test_recommended_router_tiers_have_known_offline_capacity_and_vision( limits = catalog.resolve_deployment_limits(model, provider=provider) expected_request_output = ( window // 2 - if provider == "openrouter" and output >= (window * 9) // 10 + if output >= (window * 9) // 10 else output ) assert (limits.context_window, limits.max_output_tokens) == ( @@ -181,7 +181,7 @@ def test_recommended_router_tiers_have_known_offline_capacity_and_vision( ) assert catalog.resolve_vision_support(model, provider_name=provider) == ( "supported" - if tier == "c0" or (provider == "tokenrhythm" and tier == "c1") + if tier in {"c0", "c3"} else "unsupported" ) capabilities = catalog.get_capabilities(model, provider_name=provider) diff --git a/tests/test_provider_openai_compat_payloads.py b/tests/test_provider_openai_compat_payloads.py index dc79a56788..5acd3112d4 100644 --- a/tests/test_provider_openai_compat_payloads.py +++ b/tests/test_provider_openai_compat_payloads.py @@ -246,7 +246,7 @@ def test_tokenrhythm_recommended_c1_keeps_neutral_request_dialect(monkeypatch: A model_capabilities=catalog.get_capabilities(model, provider_name="tokenrhythm"), )) payload = captured["payload"] - assert payload["model"] == "deepseek-flash" + assert payload["model"] == "deepseek-v4-flash-0731" assert payload["max_tokens"] == 384_000 assert "thinking" not in payload assert "reasoning_effort" not in payload diff --git a/tests/test_release_consistency.py b/tests/test_release_consistency.py index 8769d4362d..4f19146547 100644 --- a/tests/test_release_consistency.py +++ b/tests/test_release_consistency.py @@ -1104,7 +1104,13 @@ def test_release_workflow_hydrates_and_smokes_desktop_router_runtime() -> None: end = min(end, pos) job = workflow[start:end] assert "lfs: true" in job - assert 'git lfs pull --include="src/opensquilla/squilla_router/models/**"' in job + includes = re.search(r'git lfs pull --include="([^\"]+)"', job) + assert includes is not None + assert { + "src/opensquilla/squilla_router/models/**", + "src/opensquilla/squilla_router/lightgbm_model/assets/**", + }.issubset(includes.group(1).split(",")) + assert job.index("validate_artifacts()") < job.index("npm run build:gateway") assert "npm run build:gateway" in job assert "npm run verify:package" in job assert "npm run verify:gateway-smoke" in job diff --git a/tests/test_scripts/test_build_wheelhouse_zip.py b/tests/test_scripts/test_build_wheelhouse_zip.py index 11b9729da5..0f44c2ad18 100644 --- a/tests/test_scripts/test_build_wheelhouse_zip.py +++ b/tests/test_scripts/test_build_wheelhouse_zip.py @@ -1050,3 +1050,95 @@ def test_release_workflow_publishes_from_version_tags() -> None: assert "github.event.inputs.tag" in workflow assert "github.event_name == 'push' || github.event.inputs.tag != ''" in workflow assert "TAG: ${{ env.RELEASE_TAG }}" in workflow + + +def _router_preflight_fixture(tmp_path: Path, asset="new"): + import hashlib + import json + + module = load_script() + model_root = tmp_path / "squilla_router" / "models" + for path in module.required_router_assets(model_root): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"materialized asset") + legacy = model_root / "v4.2_phase3_inference" + current = model_root.parent / "lightgbm_model/assets" + nested = "preprocess/feature/artifacts/tokenizer.json" + payload = b'{"frozen": true}' + for root, names in ( + (legacy, ["bge_onnx/model.onnx", "bge_onnx/tokenizer.json"]), + (current, [nested]), + ): + records = [] + for name in names: + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(payload) + records.append({ + "path": name, "size_bytes": len(payload), + "sha256": hashlib.sha256(payload).hexdigest(), + }) + (root / "artifact_manifest.json").write_text( + json.dumps({"schema_version": 1, "files": records}), encoding="utf-8", + ) + target = current / nested if asset == "new" else legacy / "bge_onnx" / asset + target.unlink() + return module, model_root, target, payload + + +@pytest.mark.parametrize("asset", ["new", "model.onnx", "tokenizer.json"]) +def test_router_preflight_checks_every_manifest_asset(tmp_path: Path, asset) -> None: + module, model_root, nested_asset, payload = _router_preflight_fixture(tmp_path, asset) + assert nested_asset in module.check_router_assets(model_root).missing_files + nested_asset.parent.mkdir(parents=True, exist_ok=True) + nested_asset.write_bytes(payload) + assert module.check_router_assets(model_root).ok + nested_asset.write_text(module.LFS_POINTER_LINE + "\noid sha256:dummy\nsize 1\n") + assert nested_asset in module.check_router_assets(model_root).pointer_files + + +@pytest.mark.parametrize("asset", ["new", "model.onnx", "tokenizer.json"]) +def test_router_preflight_rejects_tampered_manifest_asset(tmp_path: Path, asset) -> None: + module, model_root, nested_asset, _ = _router_preflight_fixture(tmp_path, asset) + nested_asset.parent.mkdir(parents=True, exist_ok=True) + nested_asset.write_bytes(b"tampered") + with pytest.raises(SystemExit, match="Router artifact integrity mismatch"): + module.check_router_assets(model_root) + + +def test_router_preflight_rejects_escaping_manifest_path(tmp_path: Path) -> None: + import json + + module, model_root, _, _ = _router_preflight_fixture(tmp_path) + manifest = model_root.parent / "lightgbm_model" / "assets" / "artifact_manifest.json" + data = json.loads(manifest.read_text(encoding="utf-8")) + data["files"][0]["path"] = "../outside.json" + manifest.write_text(json.dumps(data)) + with pytest.raises(SystemExit, match="Invalid router artifact manifest"): + module.check_router_assets(model_root) + + +def test_release_wheel_permits_lightgbm_source_provenance() -> None: + module = load_script() + assert module.forbidden_release_wheel_entries([ + "opensquilla/squilla_router/lightgbm_model/PROVENANCE.md", + ]) == [] + + +def test_wheel_check_expands_lightgbm_artifact_manifest(tmp_path: Path) -> None: + import json + + module = load_script() + wheel = tmp_path / "fixture.whl" + prefix = module.LIGHTGBM_ASSET_PREFIX + extra = "preprocess/feature/artifacts/tokenizer.json" + with ZipFile(wheel, "w") as archive: + for relative in module.ROUTER_ASSET_RELS: + archive.writestr(f"{module.WHEEL_ROUTER_PREFIX}/{relative}", b"asset") + for relative in module.LIGHTGBM_ASSET_RELS: + if relative != "artifact_manifest.json": + archive.writestr(f"{prefix}/{relative}", b"asset") + archive.writestr(f"{prefix}/artifact_manifest.json", json.dumps({ + "schema_version": 1, "files": [{"path": extra}], + })) + assert module.missing_router_assets_in_wheel(wheel) == [f"{prefix}/{extra}"] diff --git a/tests/test_session/test_model_router_admission.py b/tests/test_session/test_model_router_admission.py new file mode 100644 index 0000000000..808d07403b --- /dev/null +++ b/tests/test_session/test_model_router_admission.py @@ -0,0 +1,212 @@ +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path + +import pytest + +from opensquilla.session.manager import SessionManager +from opensquilla.session.model_router_state import MODEL_DECISION_KEY, build_model_router_context +from opensquilla.session.models import TranscriptEntry +from opensquilla.session.storage import SessionStorage, StaleEpochError + + +def proposal(message_id: str, version: int = 1, tier: str = "c1") -> dict: + return { + "schema_version": 1, + "input_message_id": message_id, + "state": { + "schema_version": "model-tier-state.v1", + "policy_version": "four-tier-intent-policy.v1", + "tier": tier, + "task_id": "task", + "turn_count": version, + "version": version, + "task_start_input_message_id": message_id, + "generation_start_input_message_id": None, + }, + "decision": {"tier": tier, "intent": "new_task", "classification": "immutable"}, + } + + +def active_state(rows, node): + next_input = TranscriptEntry( + session_id=node.session_id, + session_key=node.session_key, + message_id="next", + role="user", + content="continue", + ) + return build_model_router_context( + [*rows, next_input], + bound_user_message_id="next", + exclude_last_user=True, + )["router_model_state"] + + +async def first_admitted(manager, node): + first = await manager.append_message(node.session_key, "user", "first") + await manager.stage_model_router_decision( + node.session_key, first.message_id, proposal(first.message_id) + ) + admitted = await manager.admit_model_router_decision( + node.session_key, first.message_id, expected_previous_version=None + ) + assert admitted is not None + return first, admitted + + +@pytest.mark.asyncio +async def test_rejected_stage_survives_restart_without_advancing_task(tmp_path: Path): + async with SessionStorage(tmp_path / "routing.db") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:admission") + first, admitted = await first_admitted(manager, node) + rejected = await manager.append_message(node.session_key, "user", "redo") + original = proposal(rejected.message_id, 2, "c2") + staged = await manager.stage_model_router_decision( + node.session_key, rejected.message_id, original + ) + assert staged["status"] == "staged" and staged["state_committed"] is False + assert staged["decision"] == original["decision"] + assert staged["decided_at_ms"] > 0 + await storage.close() + await storage.connect() + rows = await manager.get_model_router_transcript(node.session_key) + assert active_state(rows, node) == admitted["state"] + assert rows[-1].turn_context[MODEL_DECISION_KEY] == staged + # A duplicate request retains its first classification and timestamp. + assert ( + await manager.stage_model_router_decision( + node.session_key, rejected.message_id, proposal(rejected.message_id, 3, "c3") + ) + == staged + ) + assert rows[0].message_id == first.message_id + + +@pytest.mark.asyncio +async def test_admission_cas_rejects_competing_state_and_is_idempotent(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:cas") + await first_admitted(manager, node) + entries = [ + await manager.append_message(node.session_key, "user", text) + for text in ("first candidate", "competing candidate") + ] + for entry in entries: + await manager.stage_model_router_decision( + node.session_key, entry.message_id, proposal(entry.message_id, 2, "c2") + ) + admitted = await manager.admit_model_router_decision( + node.session_key, entries[0].message_id, expected_previous_version=1 + ) + assert admitted["state_committed"] is True + assert admitted["state_version_before"] == 1 + assert admitted["status"] == "admitted" + assert ( + await manager.admit_model_router_decision( + node.session_key, entries[1].message_id, expected_previous_version=1 + ) + is None + ) + assert ( + await manager.admit_model_router_decision( + node.session_key, entries[0].message_id, expected_previous_version=1 + ) + == admitted + ) + with pytest.raises(StaleEpochError): + await manager.admit_model_router_decision( + node.session_key, + entries[0].message_id, + expected_previous_version=1, + expected_session_id="replaced", + ) + with pytest.raises(StaleEpochError): + await manager.admit_model_router_decision( + node.session_key, + entries[0].message_id, + expected_previous_version=1, + expected_session_id=node.session_id, + expected_session_epoch=1, + ) + rows = await manager.get_model_router_transcript(node.session_key) + assert active_state(rows, node) == admitted["state"] + + +@pytest.mark.asyncio +async def test_quality_cas_ignores_staged_candidates_and_never_upgrades_them(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:quality-admission") + first, admitted = await first_admitted(manager, node) + rejected = await manager.append_message(node.session_key, "user", "candidate") + staged = await manager.stage_model_router_decision( + node.session_key, rejected.message_id, proposal(rejected.message_id, 2, "c2") + ) + args = { + "expected_session_id": node.session_id, + "expected_session_epoch": node.epoch, + "expected_version": 1, + "target_tier": "c2", + "reason": "no_progress", + "scope_id": "quality", + "execution_update": {"tier": "c2", "model": "model", "provider": "provider"}, + } + upgraded = await manager.advance_model_router_quality_retry( + node.session_key, first.message_id, **args + ) + assert upgraded["state"]["version"] == 2 + assert upgraded["decision"] == admitted["decision"] + assert ( + await manager.admit_model_router_decision( + node.session_key, first.message_id, expected_previous_version=None + ) + == upgraded + ) + assert ( + await manager.admit_model_router_decision( + node.session_key, rejected.message_id, expected_previous_version=1 + ) + is None + ) + staged_args = deepcopy(args) + staged_args.update(expected_version=2, target_tier="c3", execution_update={"tier": "c3"}) + assert ( + await manager.advance_model_router_quality_retry( + node.session_key, rejected.message_id, **staged_args + ) + is None + ) + rows = await manager.get_model_router_transcript(node.session_key) + assert rows[-1].turn_context[MODEL_DECISION_KEY] == staged + + +@pytest.mark.asyncio +async def test_admission_follows_accepted_row_into_compaction_archive(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:archived-admission") + accepted = await manager.append_message(node.session_key, "user", "request") + staged = await manager.stage_model_router_decision( + node.session_key, accepted.message_id, proposal(accepted.message_id) + ) + rows = await storage.get_transcript(node.session_id) + await storage.rewrite_compacted_session( + node=node, + summary=None, + entries=[], + archived_entries=rows, + expected_session_id=node.session_id, + expected_session_epoch=0, + ) + admitted = await manager.admit_model_router_decision( + node.session_key, accepted.message_id, expected_previous_version=None + ) + assert admitted["decided_at_ms"] == staged["decided_at_ms"] + assert admitted["admitted_at_ms"] >= admitted["decided_at_ms"] + rows = await manager.get_model_router_transcript(node.session_key) + assert rows[0].turn_context[MODEL_DECISION_KEY] == admitted + assert active_state(rows, node) == admitted["state"] diff --git a/tests/test_session/test_model_router_state.py b/tests/test_session/test_model_router_state.py new file mode 100644 index 0000000000..aee5642d72 --- /dev/null +++ b/tests/test_session/test_model_router_state.py @@ -0,0 +1,585 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from opensquilla.engine.runtime import TurnRunner +from opensquilla.gateway.config import GatewayConfig +from opensquilla.session.manager import SessionManager +from opensquilla.session.model_router_state import ( + MODEL_DECISION_KEY, + build_model_router_context, + model_generation_transcript, +) +from opensquilla.session.models import ( + AgentTaskRecord, + AgentTaskStatus, + SessionNode, + TranscriptEntry, +) +from opensquilla.session.storage import SessionStorage, StaleEpochError + + +def record( + input_id: str, + tier: str = "c2", + *, + task_id: str = "task-a", + count: int = 1, + task_start: str | None = None, + generation_start: str | None = None, + version: int = 1, +) -> dict: + return { + "schema_version": 1, + "status": "admitted", + "state_committed": True, + "input_message_id": input_id, + "state": { + "schema_version": "model-tier-state.v1", + "policy_version": "four-tier-intent-policy.v1", + "tier": tier, + "task_id": task_id, + "turn_count": count, + "version": version, + "task_start_input_message_id": task_start or input_id, + "generation_start_input_message_id": generation_start, + }, + "decision": {"tier": tier, "intent": "new_task", "audit": "test"}, + } + + +async def admit(manager, session_key, message_id, decision, **kwargs): + await manager.stage_model_router_decision(session_key, message_id, decision, **kwargs) + result = await manager.admit_model_router_decision( + session_key, + message_id, + expected_previous_version=decision["state"]["version"] - 1 or None, + **kwargs, + ) + assert result is not None + return result + + +def user(mid: str, text: str, decision: dict | None = None, *, turn_id: str | None = None): + context = {"turn_id": turn_id or mid} + if decision: + context[MODEL_DECISION_KEY] = decision + return TranscriptEntry( + session_id="sid", + session_key="agent:main:router", + role="user", + message_id=mid, + content=text, + turn_context=context, + ) + + +def context(rows, bound: str): + return build_model_router_context( + rows, bound_user_message_id=bound, exclude_last_user=True + ) + + +def test_state_survives_long_gap_and_missing_assistant_without_future_leak(): + old = user("old", "task anchor " + "x" * 8000, record("old")) + old.created_at = 1 + current = user("now", "continue") + future = user("future", "future secret", record("future", "c3")) + result = context([old, current, future], "now") + assert result["router_model_state"] == old.turn_context[MODEL_DECISION_KEY]["state"] + assert result["router_model_input"]["task_anchor"] == old.content + assert result["router_model_input"]["history_user"] == [old.content] + assert result["router_model_input"]["previous_outcome"] == "unknown" + + +def test_cross_task_history_full_usage_and_queued_previous_reply(): + first = user("a", "old task", record("a")) + second = user("b", "new task " + "z" * 9000, record("b", "c1", task_id="task-b")) + current = user("c", "continue") + future = user("d", "future") + assistant = TranscriptEntry( + session_id="sid", + session_key=first.session_key, + role="assistant", + content="answer " + "r" * 15000, + turn_context={"turn_id": "b"}, + turn_usage={ + "model": "model-actual", + "routed_tier": "c1", + "output_tokens": 3000, + "reasoning_tokens": 1200, + "input_tokens": 2400, + "cached_tokens": 0, + "cache_write_tokens": 0, + }, + ) + result = context([first, second, current, future, assistant], "c")["router_model_input"] + assert result["history_user"] == [first.content, second.content] + assert result["task_anchor"] == second.content + assert result["previous_answer"] == assistant.content + assert result["previous_usage"] == assistant.turn_usage + + +def test_fork_aliases_restore_task_anchor_and_explicit_generation_boundary(): + old = user("fork-old", "old", record("old")) + new = user("fork-new", "new", record("new", task_id="new", generation_start="new")) + follow = user( + "fork-follow", + "continue", + record("follow", task_id="new", count=2, task_start="new", generation_start="new"), + ) + current = user("current", "again") + rows = [old, new, follow, current] + result = context(rows, "current") + assert result["router_model_input"]["task_anchor"] == "new" + assert result["router_model_input"]["history_user"] == ["old", "new", "continue"] + assert model_generation_transcript(rows, "current") == [new, follow, current] + + +def test_replay_decision_is_separate_from_prior_state_and_future_assistant(): + prior = user("prior", "first", record("prior", "c1")) + current = user("current", "redo", record("current", "c2", count=2, task_start="prior")) + assistant = TranscriptEntry( + session_id="sid", + session_key=prior.session_key, + role="assistant", + content="current answer", + turn_context={"turn_id": "current"}, + ) + result = context([prior, current, assistant], "current") + assert result["router_model_state"]["tier"] == "c1" + assert result["router_model_decision"] == current.turn_context[MODEL_DECISION_KEY] + assert result["router_model_input"]["previous_answer"] == "" + + +def test_missing_bound_input_fails_closed(): + with pytest.raises(RuntimeError, match="absent"): + context([user("a", "not current")], "missing") + + +def test_feature_usage_requires_complete_counters_and_strips_execution_metadata(): + from opensquilla.session.model_router_state import canonical_previous_usage + + counters = { + "input_tokens": 1, + "output_tokens": 2, + "reasoning_tokens": 0, + "cached_tokens": 0, + "cache_write_tokens": 0, + } + assert canonical_previous_usage( + { + **counters, + "model": "actual", + "route_id": "route", + "execution_status": "failed", + "retry_count": 2, + } + ) == { + **counters, + "model": "actual", + } + assert canonical_previous_usage({"output_tokens": 2}) == {} + assert canonical_previous_usage({**counters, "cached_tokens": True}) == {} + + +def test_blank_attachment_inputs_do_not_consume_feature_history_slots(): + rows = [user(str(i), str(i) * (i + 1)) for i in range(6)] + rows.append(user("empty", '{"text":" ","attachments":[{"mime":"image/png"}]}')) + rows.append(user("current", "now")) + result = context(rows, "current") + assert result["router_model_input"]["history_user"] == ["222", "3333", "44444", "555555"] + assert result["router_model_history_user_chars"] == 21 + + +@pytest.mark.asyncio +async def test_stage_survives_restart_is_idempotent_and_preserves_lifecycle_context( + tmp_path: Path, +): + path = tmp_path / "sessions.db" + async with SessionStorage(path) as storage: + node = SessionNode(session_key="agent:main:router", session_id="sid") + await storage.upsert_session(node) + entry = user("accepted", "request") + entry.turn_context["router_model_control_event"] = "new_task" + await storage.append_transcript_entry(entry, expected_epoch=0) + first = { + **record("accepted"), + "status": "staged", + "state_committed": False, + "decided_at_ms": 12345, + } + assert ( + await storage.stage_model_router_decision( + node.session_key, + entry.message_id, + first, + expected_session_id=node.session_id, + expected_epoch=0, + ) + == first + ) + assert ( + await storage.stage_model_router_decision( + node.session_key, + entry.message_id, + record("accepted", "c3"), + expected_session_id=node.session_id, + expected_epoch=0, + ) + == first + ) + await storage.update_transcript_turn_context( + node.session_key, + entry.message_id, + dict(turn_id="promoted", disposition="promoted", router_model_control_event="redo"), + ) + await storage.close() + await storage.connect() + rows = await storage.get_canonical_transcript(node.session_id) + assert rows[0].turn_context == { + "turn_id": "promoted", + "disposition": "promoted", + "router_model_control_event": "new_task", + MODEL_DECISION_KEY: first, + } + with pytest.raises(StaleEpochError): + await storage.stage_model_router_decision( + node.session_key, + entry.message_id, + first, + expected_session_id="replaced-session", + expected_epoch=0, + ) + with pytest.raises(RuntimeError, match="no accepted"): + await storage.stage_model_router_decision( + node.session_key, + "absent", + first, + expected_session_id=node.session_id, + expected_epoch=0, + ) + + +@pytest.mark.asyncio +async def test_runtime_reads_canonical_model_state_with_complete_usage(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage) + node = await manager.create("agent:main:router") + accepted = await manager.append_message(node.session_key, "user", "earlier") + await admit(manager, node.session_key, accepted.message_id, record(accepted.message_id)) + await manager.append_message( + node.session_key, + "assistant", + "answer", + turn_usage={ + "output_tokens": 100, + "reasoning_tokens": 90, + "input_tokens": 200, + "cached_tokens": 0, + "cache_write_tokens": 0, + "model": "actual-model", + }, + ) + current = await manager.append_message(node.session_key, "user", "continue") + runner = TurnRunner( + provider_selector=MagicMock(), session_manager=manager, config=GatewayConfig() + ) + result = await runner._router_previous_assistant_context( + node.session_key, + exclude_last_user=True, + bound_user_message_id=current.message_id, + expected_session_id=node.session_id, + expected_session_epoch=0, + ) + assert result["router_model_state"]["tier"] == "c2" + assert result["router_model_input"]["previous_usage"]["reasoning_tokens"] == 90 + + +@pytest.mark.asyncio +async def test_pipeline_commits_state_before_capacity_error_and_observe_does_not( + monkeypatch, + tmp_path: Path, +): + from opensquilla.engine import pipeline, steps + + async with SessionStorage(tmp_path / "sessions.db") as storage: + manager = SessionManager(storage) + node = await manager.create("agent:main:model-commit") + entry = await manager.append_message(node.session_key, "user", "request") + envelope = record(entry.message_id) + apply = True + + async def fake_pipeline(turn, _steps, **kwargs): + turn.metadata.update( + { + "routing_applied": apply, + "router_model_decision": envelope, + } + ) + return turn + + async def fail_capacity(turn, **kwargs): + raise ValueError("physical capacity denied") + + monkeypatch.setattr(pipeline, "run_pipeline", fake_pipeline) + monkeypatch.setattr(steps, "finalize_squilla_router_capacity", fail_capacity) + runner = TurnRunner( + provider_selector=MagicMock(), session_manager=manager, config=GatewayConfig() + ) + for apply in (False, True): + with pytest.raises(ValueError, match="physical capacity"): + await runner._run_pipeline( + "request", node.session_key, None, None, [], "", [], + bound_user_message_id=entry.message_id, + ) + after = await storage.get_canonical_transcript(node.session_id) + if not apply: + assert MODEL_DECISION_KEY not in (after[0].turn_context or {}) + persisted = after[0].turn_context[MODEL_DECISION_KEY] + assert persisted["state"] == envelope["state"] + assert persisted["status"] == "staged" + assert persisted["state_committed"] is False + next_input = user("next", "continue") + assert context([*after, next_input], "next")["router_model_state"] is None + assert persisted["decision"] == envelope["decision"] + assert persisted["decided_at_ms"] > 0 + with pytest.raises(RuntimeError, match="durable accepted"): + await runner._run_pipeline("request", node.session_key, None, None, [], "", []) + + +@pytest.mark.asyncio +async def test_canonical_compaction_and_prefix_fork_keep_only_owned_state(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:canonical-state") + first = await manager.append_message(node.session_key, "user", "anchor") + first_record = record(first.message_id) + first_record = await admit(manager, node.session_key, first.message_id, first_record) + current = await manager.append_message(node.session_key, "user", "current") + rows = await storage.get_transcript(node.session_id) + await storage.rewrite_compacted_session( + node=node, + summary=None, + entries=[rows[1]], + archived_entries=[rows[0]], + expected_session_id=node.session_id, + expected_session_epoch=0, + ) + canonical = await manager.get_model_router_transcript(node.session_key) + assert context(canonical, current.message_id)["router_model_state"] == first_record["state"] + assert ( + await admit( + manager, + node.session_key, + first.message_id, + record(first.message_id, "c3"), + ) + == first_record + ) + await admit( + manager, + node.session_key, + current.message_id, + record(current.message_id, "c3", version=2), + ) + fork = await manager.prepare_prefix_branch( + node.session_key, + "agent:main:canonical-fork", + fork_before_message_id=current.message_id, + ) + child_rows = list(fork.initial_transcript_entries) + child_current = user("child-new", "continue") + recovered = context([*child_rows, child_current], child_current.message_id) + assert recovered["router_model_state"]["tier"] == "c2" + assert recovered["router_model_input"]["task_anchor"] == "anchor" + + +@pytest.mark.asyncio +async def test_previous_outcome_uses_terminal_ledger_when_reply_missing(): + async with SessionStorage(":memory:") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:failed-state") + first = await manager.append_message(node.session_key, "user", "request") + await manager.update_message_turn_context( + node.session_key, first.message_id, {"turn_id": "failed-turn"} + ) + await admit(manager, node.session_key, first.message_id, record(first.message_id)) + await storage.create_agent_task( + AgentTaskRecord( + task_id="failed-turn", + session_key=node.session_key, + status=AgentTaskStatus.FAILED, + details={ + "turn_id": "failed-turn", + "turn_outcome": {"kind": "failed", "reason": "provider"}, + }, + ) + ) + current = await manager.append_message(node.session_key, "user", "retry") + rows = await manager.get_model_router_transcript(node.session_key) + assert ( + context(rows, current.message_id)["router_model_input"]["previous_outcome"] == "failure" + ) + + +def test_source_route_features_and_explicit_reset_keep_cross_task_input(): + import time + + prior = user("a", " earlier text ", record("a", "c1")) + prior.turn_context[MODEL_DECISION_KEY].update( + { + "decided_at_ms": int(time.time() * 1000), + "updated_at_ms": int(time.time() * 1000) + 1, + "decision": { + "tier_audit": {"probabilities": {"c0": 0.1, "c1": 0.6, "c2": 0.2, "c3": 0.1}} + }, + } + ) + current = user("b", "new task") + current.turn_context["router_model_control_event"] = "new_task" + result = context([prior, current], "b") + assert result["router_model_input"]["history_user"] == ["earlier text"] + assert result["router_model_input"]["route_history"] == [ + {"tier_id": "C1", "difficulty": pytest.approx(1.3), "margin": pytest.approx(0.4)}, + ] + control = result["router_model_control_context"] + assert control["context_tokens_est"] == 0 + + +def test_failed_latest_route_does_not_pair_an_older_answer_and_attachments_are_normalized(): + from opensquilla.session.model_router_state import normalized_router_attachments + + old = user("old", "old", record("old")) + answer = TranscriptEntry( + session_id="sid", + session_key=old.session_key, + role="assistant", + content="old answer", + turn_context={"turn_id": "old"}, + ) + failed = user("failed", "failed", record("failed")) + now = user("now", "continue") + assert context([old, answer, failed, now], "now")["router_model_input"]["previous_answer"] == "" + assert normalized_router_attachments( + [{"mime": "image/png", "name": "private.png", "data": "private bytes", "token_count": 123}] + ) == [ + { + "type": "image", + "mime_type": None, + "media_type": None, + "parse_status": None, + "status": None, + "summary": None, + "truncated": None, + "token_count": 123, + } + ] + assert normalized_router_attachments([{"mime_type": "application/pdf"}, {}]) == [] + + +@pytest.mark.asyncio +async def test_quality_retry_cas_retains_prediction_and_restart_uses_final_tier(tmp_path: Path): + async with SessionStorage(tmp_path / "quality.db") as storage: + manager = SessionManager(storage, inject_time_prefix=False) + node = await manager.create("agent:main:quality") + accepted = await manager.append_message(node.session_key, "user", "request") + original = record(accepted.message_id, "c1") + await admit(manager, node.session_key, accepted.message_id, original) + args = dict( + expected_session_id=node.session_id, + expected_session_epoch=0, + expected_version=1, + target_tier="c2", + reason="no_progress", + scope_id="scope", + execution_update={ + "tier": "c2", + "model": "retry-model", + "provider": "test", + "thinking_mode": "T2", + "prompt_policy": "P1", + "thinking_level": "medium", + }, + ) + advanced = await manager.advance_model_router_quality_retry( + node.session_key, + accepted.message_id, + **args, + ) + assert advanced["decision"] == original["decision"] + assert advanced["state"] == {**original["state"], "tier": "c2", "version": 2} + assert advanced["quality_retry"]["used"] is True + assert ( + await manager.advance_model_router_quality_retry( + node.session_key, + accepted.message_id, + **args, + ) + is None + ) + await storage.close() + await storage.connect() + current = await manager.append_message(node.session_key, "user", "continue") + rows = await manager.get_model_router_transcript(node.session_key) + recovered = context(rows, current.message_id) + assert recovered["router_model_state"]["tier"] == "c2" + with pytest.raises(StaleEpochError): + await manager.advance_model_router_quality_retry( + node.session_key, + accepted.message_id, + **{**args, "expected_session_id": "wrong"}, + ) + + +@pytest.mark.asyncio +async def test_runtime_model_input_matches_source_projection(monkeypatch): + from types import SimpleNamespace + + from opensquilla.engine import pipeline, steps + + captured = {} + + async def capture(turn, _steps, **kwargs): + captured.update(turn.metadata) + return turn + + async def stop(turn, **kwargs): + raise ValueError("capture boundary") + + monkeypatch.setattr(pipeline, "run_pipeline", capture) + monkeypatch.setattr(steps, "finalize_squilla_router_capacity", stop) + runner = TurnRunner(provider_selector=MagicMock(), config=GatewayConfig()) + partial = { + "task_anchor": "a" * 8000, + "history_user": ["12345"], + "previous_answer": "abcdef", + "route_history": [{"tier_id": "C1"}], + "previous_usage": {"reasoning_tokens": 99}, + } + with pytest.raises(ValueError, match="capture boundary"): + await runner._run_pipeline( + "physical wrapper", + "session", + None, + None, + [{"name": "z"}, SimpleNamespace(name="a"), {"name": "z"}], + "", + [], + semantic_message="raw request", + model_router_context={ + "router_model_input": partial, + "router_model_control_context": {"context_tokens_est": 17}, + "router_model_history_user_chars": 100, + }, + ) + projected = captured["router_model_input"] + assert projected["current_request"] == "raw request" + assert projected["task_anchor"] == "a" * 8000 + assert projected["context"] == {"turn_index": 1, "context_tokens_est": 29} + assert projected["tool_state"] == {"available_tools": ["a", "z"]} + assert projected["previous_usage"] == {} + assert captured["router_model_control_context"]["context_tokens_est"] == 20 diff --git a/tests/test_telemetry/test_client_runtime.py b/tests/test_telemetry/test_client_runtime.py index 51cbd60f09..1c58ea4fcf 100644 --- a/tests/test_telemetry/test_client_runtime.py +++ b/tests/test_telemetry/test_client_runtime.py @@ -817,11 +817,13 @@ async def stalled(_request): offline_uploads.handler = stalled runtime = ScopedTelemetryRuntime(config=_config(tmp_path)) closing = None + # Reaching the upload includes SQLite claim I/O; only cancellation after + # the explicit deadline below is subject to the one-second assertion. try: assert (await runtime.record(_turn_event())).status is RecordStatus.RECORDED if start_upload: await runtime.start() - await asyncio.wait_for(entered.wait(), timeout=1) + await asyncio.wait_for(entered.wait(), timeout=10) assert (runtime._upload_task is not None) is start_upload monkeypatch.setattr(runtime_module, "SHUTDOWN_UPLOAD_TIMEOUT_SECONDS", 0.05) if pause_before_guard: @@ -838,15 +840,20 @@ def preempted_shutdown(): monkeypatch.setattr(runtime, "prepare_shutdown", preempted_shutdown) closing = asyncio.create_task(runtime.close()) if not start_upload: - await asyncio.wait_for(entered.wait(), timeout=1) + await asyncio.wait_for(entered.wait(), timeout=10) assert not cancelled.is_set() timeouts = await expire_shutdown_deadline(runtime, cancel=not cancel_close) if cancel_close: closing.cancel() + # Bound the upload cancellation separately from durable SQLite cleanup; + # disk-worker scheduling must not turn a completed network cancellation + # into a second cancellation of close(). + await asyncio.wait_for(cancelled.wait(), timeout=1) + if cancel_close: with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(closing, timeout=1) + await asyncio.wait_for(closing, timeout=10) else: - await asyncio.wait_for(closing, timeout=1) + await asyncio.wait_for(closing, timeout=10) assert any(timeout.expired() for timeout in timeouts) is not cancel_close assert cancelled.is_set() diff --git a/tests/test_tools/test_router_control_tool.py b/tests/test_tools/test_router_control_tool.py index 7ad7a48d7d..eebf77a00f 100644 --- a/tests/test_tools/test_router_control_tool.py +++ b/tests/test_tools/test_router_control_tool.py @@ -51,8 +51,8 @@ async def test_router_control_set_hold_writes_store_and_requests_replay() -> Non assert result.terminates_turn is True assert payload["accepted"] is True assert payload["target_tier"] == "c3" - assert payload["target_model"] == "z-ai/glm-5.3" - assert payload["target_label"] == "glm-5.3" + assert payload["target_model"] == "moonshotai/kimi-k3" + assert payload["target_label"] == "kimi-k3" assert payload["target_execution_kind"] == "single_model" assert payload["target_model_role"] == "selected_model" assert payload["replay_required"] is True @@ -80,14 +80,14 @@ async def test_router_control_ensemble_target_keeps_anchor_but_reports_fusion() ) payload = json.loads(result.content) - assert payload["target_model"] == "z-ai/glm-5.3" + assert payload["target_model"] == "moonshotai/kimi-k3" assert payload["target_provider"] == "openrouter" assert payload["target_label"] == "multi-model fusion" assert payload["target_execution_kind"] == "ensemble" assert payload["target_model_role"] == "ensemble_anchor" hold = ctx.router_control_hold_store.get_valid(ctx.session_key or "") assert hold is not None - assert hold.model == "z-ai/glm-5.3" + assert hold.model == "moonshotai/kimi-k3" assert hold.provider == "openrouter" @@ -126,7 +126,7 @@ def test_router_control_tool_schema_includes_dynamic_target_enum() -> None: assert "enum" in target_schema assert "tier:c3" in target_schema["enum"] assert "tier:t3" not in target_schema["enum"] - assert "model:z-ai/glm-5.3" not in target_schema["enum"] + assert "model:moonshotai/kimi-k3" not in target_schema["enum"] def test_router_control_tool_is_hidden_when_router_is_disabled() -> None: diff --git a/uv.lock b/uv.lock index 34547f2468..c896dd11dc 100644 --- a/uv.lock +++ b/uv.lock @@ -1932,6 +1932,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "msoffcrypto-tool" version = "6.0.0" @@ -2256,34 +2265,35 @@ wheels = [ [[package]] name = "onnxruntime" -version = "1.30.0" +version = "1.24.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "flatbuffers" }, { name = "numpy" }, { name = "packaging" }, { name = "protobuf" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/6f/48169f2e62b405bff5053cbd1d73fb5ce41ef7ecd13bb3bfcc191e689b8a/onnxruntime-1.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd", size = 21544867, upload-time = "2026-09-10T16:31:19.876Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/cbc5b8f91963689fdd622f463508c01d0aa95d3f944747b1e0b1eb2160b8/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83", size = 21345202, upload-time = "2026-09-10T16:31:23.361Z" }, - { url = "https://files.pythonhosted.org/packages/34/35/e7f862dbacbc99fadd9b14a614e49c99bf0f35fd9927a82f096e3de33531/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328", size = 23585654, upload-time = "2026-09-10T16:31:26.65Z" }, - { url = "https://files.pythonhosted.org/packages/a6/13/0f1699f6de549c9324bc9112a2a85b14c517904cd11b562a654643b755a1/onnxruntime-1.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38", size = 14311470, upload-time = "2026-09-10T16:31:31.273Z" }, - { url = "https://files.pythonhosted.org/packages/a2/17/02b13e5f51461f0453b18ab854e2d0bc1b6ec353241b05a2ee6b79e35d87/onnxruntime-1.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262", size = 14175072, upload-time = "2026-09-10T16:31:34.541Z" }, - { url = "https://files.pythonhosted.org/packages/f0/75/508454c5d01f31641dabc597fe559594c931a520a2673031179319d0afd8/onnxruntime-1.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134", size = 21544990, upload-time = "2026-09-10T16:31:37.685Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/e603c71f43f4fe3fd156a053af79cbed6e27a2c649f0988a67d97fedd39f/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9", size = 21344996, upload-time = "2026-09-10T16:31:40.915Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a1/ede48ab5dc54907a2999362777f541e132639fb06628ded1932058aa8a36/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f", size = 23585560, upload-time = "2026-09-10T16:31:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/3c/dd/c57c529dbc6dd55eca24b12cfbeab1b6a690de72083824eca689085f55b0/onnxruntime-1.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93", size = 14311378, upload-time = "2026-09-10T16:31:48.034Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/ef00b45b911e7f2115273a24ed1b43e73c8c6c9ec17f9bfa13f8492581f8/onnxruntime-1.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f", size = 14175134, upload-time = "2026-09-10T16:31:50.799Z" }, - { url = "https://files.pythonhosted.org/packages/92/0a/284fd6fe701c9a8aff39dbc119c37582ca40e9cff1ea05425a7cc8606a02/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e", size = 21349453, upload-time = "2026-09-10T16:31:53.898Z" }, - { url = "https://files.pythonhosted.org/packages/d6/72/4f4466f8fa1ec267a9ef5e2f3bd175c203d3da0facbdf4049dc93abbe91c/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca", size = 23579837, upload-time = "2026-09-10T16:31:56.82Z" }, - { url = "https://files.pythonhosted.org/packages/6a/03/05c9a9234688757d2876ddecf80bba908561ee11debf125bc1a427ae6f48/onnxruntime-1.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547", size = 21545313, upload-time = "2026-09-10T16:31:59.654Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bc/1069e58b24779ba9d2fd479db5ecb3a15a6f49b585107c898819c0789558/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175", size = 21346830, upload-time = "2026-09-10T16:32:02.275Z" }, - { url = "https://files.pythonhosted.org/packages/f1/38/8138eed225c5bc6ddfc05879ecac7dacc63c34b9b6f99be72839c1f6dc49/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec", size = 23586333, upload-time = "2026-09-10T16:32:04.996Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0a/748b86000fbf9518b5dfc5bdc1b924eeaca976203740fd9d30649e568ee6/onnxruntime-1.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4", size = 14675319, upload-time = "2026-09-10T16:32:07.5Z" }, - { url = "https://files.pythonhosted.org/packages/2f/db/db8e4c0cf6f70f1311060560630fd21763648e01dfbba5195a2592c052ef/onnxruntime-1.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d", size = 14569360, upload-time = "2026-09-10T16:32:09.896Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/ec0a9d656e39b43887c378a5388b20c3b1e1ee43bded84580b0936466694/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294", size = 21348433, upload-time = "2026-09-10T16:32:12.772Z" }, - { url = "https://files.pythonhosted.org/packages/91/f0/40f74b7c00077e1e25627067ed98a70df1fef5c0e21b82849190d312554e/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7", size = 23579340, upload-time = "2026-09-10T16:32:15.532Z" }, + { name = "sympy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/38/31db1b232b4ba960065a90c1506ad7a56995cd8482033184e97fadca17cc/onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78", size = 17341875, upload-time = "2026-03-17T22:05:51.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/60/c4d1c8043eb42f8a9aa9e931c8c293d289c48ff463267130eca97d13357f/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5", size = 15172485, upload-time = "2026-03-17T22:03:32.182Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/5b68110e0460d73fad814d5bd11c7b1ddcce5c37b10177eb264d6a36e331/onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c", size = 17244912, upload-time = "2026-03-17T22:04:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f4/6b89e297b93704345f0f3f8c62229bee323ef25682a3f9b4f89a39324950/onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb", size = 12596856, upload-time = "2026-03-17T22:05:41.224Z" }, + { url = "https://files.pythonhosted.org/packages/43/06/8b8ec6e9e6a474fcd5d772453f627ad4549dfe3ab8c0bf70af5afcde551b/onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90", size = 12270275, upload-time = "2026-03-17T22:05:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/8a21ec0a97e40abb7d8da1e8b20fb9e1af509cc6d191f6faa75f73622fb2/onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0", size = 17341922, upload-time = "2026-03-17T22:03:56.364Z" }, + { url = "https://files.pythonhosted.org/packages/8b/25/d7908de8e08cee9abfa15b8aa82349b79733ae5865162a3609c11598805d/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13", size = 15172290, upload-time = "2026-03-17T22:03:37.124Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/105ec27a78c5aa0154a7c0cd8c41c19a97799c3b12fc30392928997e3be3/onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f", size = 17244738, upload-time = "2026-03-17T22:04:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/fb/a592736d968c2f58e12de4d52088dda8e0e724b26ad5c0487263adb45875/onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93", size = 12597435, upload-time = "2026-03-17T22:05:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/ad/04/ae2479e9841b64bd2eb44f8a64756c62593f896514369a11243b1b86ca5c/onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19", size = 12269852, upload-time = "2026-03-17T22:05:33.353Z" }, + { url = "https://files.pythonhosted.org/packages/b4/af/a479a536c4398ffaf49fbbe755f45d5b8726bdb4335ab31b537f3d7149b8/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee", size = 15176861, upload-time = "2026-03-17T22:03:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/be/13/19f5da70c346a76037da2c2851ecbf1266e61d7f0dcdb887c667210d4608/onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36", size = 17247454, upload-time = "2026-03-17T22:04:46.643Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/b30dbbd6037847b205ab75d962bc349bf1e46d02a65b30d7047a6893ffd6/onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4", size = 17343300, upload-time = "2026-03-17T22:03:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/61/88/1746c0e7959961475b84c776d35601a21d445f463c93b1433a409ec3e188/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1", size = 15175936, upload-time = "2026-03-17T22:03:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ba/4699cde04a52cece66cbebc85bd8335a0d3b9ad485abc9a2e15946a1349d/onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177", size = 17246432, upload-time = "2026-03-17T22:04:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/4590910841bb28bd3b4b388a9efbedf4e2d2cca99ddf0c863642b4e87814/onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858", size = 12903276, upload-time = "2026-03-17T22:05:46.349Z" }, + { url = "https://files.pythonhosted.org/packages/7f/6f/60e2c0acea1e1ac09b3e794b5a19c166eebf91c0b860b3e6db8e74983fda/onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d", size = 12594365, upload-time = "2026-03-17T22:05:35.795Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/0c05d10f8f6c40fe0912ebec0d5a33884aaa2af2053507e864dab0883208/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661", size = 15176889, upload-time = "2026-03-17T22:03:48.021Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1d/1666dc64e78d8587d168fec4e3b7922b92eb286a2ddeebcf6acb55c7dc82/onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731", size = 17247021, upload-time = "2026-03-17T22:04:52.377Z" }, ] [[package]] @@ -2462,7 +2472,7 @@ requires-dist = [ { name = "multidict", specifier = ">=6.9.1" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.1" }, { name = "numpy", marker = "extra == 'recommended'", specifier = ">=2.5.3" }, - { name = "onnxruntime", marker = "extra == 'recommended'", specifier = ">=1.30.0" }, + { name = "onnxruntime", marker = "extra == 'recommended'", specifier = "==1.24.4" }, { name = "openpyxl", specifier = ">=3.1.0" }, { name = "pdfplumber", specifier = ">=0.11.10" }, { name = "pillow", specifier = ">=13.0.0" }, @@ -2487,7 +2497,7 @@ requires-dist = [ { name = "reportlab", specifier = ">=4.0.0" }, { name = "rich", specifier = ">=13.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.16.7" }, - { name = "scikit-learn", marker = "extra == 'recommended'", specifier = ">=1.9.1" }, + { name = "scikit-learn", marker = "extra == 'recommended'", specifier = "==1.8.0" }, { name = "slack-sdk", specifier = ">=3.44.1,<4" }, { name = "snowballstemmer", specifier = ">=2.2" }, { name = "soupsieve", specifier = ">=2.9.2" }, @@ -3711,53 +3721,46 @@ wheels = [ [[package]] name = "scikit-learn" -version = "1.9.1" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, - { name = "narwhals" }, { name = "numpy" }, { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/a7/25f0a43d2fde306e8ef45f45121192f687b79beaf4bae8c21607c46c5e63/scikit_learn-1.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c0f8b5d09b44101cea2767f300680bada1ea27f976fe4b48b83950a4f55a49a", size = 8775209, upload-time = "2026-09-10T18:32:42.804Z" }, - { url = "https://files.pythonhosted.org/packages/60/ea/57e57539ce175d774fc291ed091b0a6d756854b92cd92554c6bb4d0ae498/scikit_learn-1.9.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:8c14ce41d561f7749f990b41d6703fe02c4669fbc485e598e069e0a1967b488e", size = 8295541, upload-time = "2026-09-10T18:32:45.069Z" }, - { url = "https://files.pythonhosted.org/packages/78/2b/5721a174406bfba49bce20ae997b3b64cf355c3f623a2638284ab6a82156/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4c20a6c017d820faa7ac8c783e3d0c6a9a2e297bf9f55332ca17cdf7fd4d04d", size = 8871592, upload-time = "2026-09-10T18:32:46.999Z" }, - { url = "https://files.pythonhosted.org/packages/8e/57/a50162f3d29feb979ab6347c6debda506dfb525bcff3c50dd17606651c7e/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5d7b18a5b9dca241a74695f3275fa4c895a9dadc72b3d8df5fa9d1083c9b83e", size = 9166043, upload-time = "2026-09-10T18:32:49.343Z" }, - { url = "https://files.pythonhosted.org/packages/72/8d/27c054166bac671770d1ea0ef7716134fe8119c0df224a58c89fd735a61c/scikit_learn-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4b59abb30618121cc46b45972d6bf53a7128b4df4cd346c6ca6f4d5f9031e49c", size = 8262238, upload-time = "2026-09-10T18:32:51.679Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d6/493086006ea0c68ad62c40a8dece1961b61bf503f45400f133d47f56e5be/scikit_learn-1.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:d5945a2908be62350e2978344e62b56c1552c2ca4f844ebf6277c94944d647dd", size = 7902102, upload-time = "2026-09-10T18:32:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8d/b60d5e7354ff0ff5cc9400e60273696589d87a30b8b2235886a76d80d062/scikit_learn-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c2b312fd8c02951a364fa120ea08c1cec10d863466bf1701b013152d7537835", size = 8739300, upload-time = "2026-09-10T18:32:55.483Z" }, - { url = "https://files.pythonhosted.org/packages/2f/81/3c6392c03665d2899457a76e535a9a6f597dddddf3220fd2e1d790da88c5/scikit_learn-1.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:61cd968ab831a76d0ecbaf0347ab2270268716da28f94fd022497e3d6f205f13", size = 8262364, upload-time = "2026-09-10T18:32:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/0f/35/a15b8653499692879821301d48059376d6e68e8b65cd0f22d19b6ee83cd9/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5990f9c69e431bfaddcde1a6d7c5355243e026bc9b9e560c13893b90dab53fb4", size = 8823698, upload-time = "2026-09-10T18:33:00.632Z" }, - { url = "https://files.pythonhosted.org/packages/23/e5/688703d357e5393f708d98eb189fd415ae69e39f6de03c6bd4005aef6118/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e79d6e9b0923f1a978179822bd43d7f5543f45e970a00fe861f43486380aba", size = 9121732, upload-time = "2026-09-10T18:33:02.825Z" }, - { url = "https://files.pythonhosted.org/packages/96/45/a10add34c08184d373be9384660c75758128ca881ed27b503b6f6a742478/scikit_learn-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:2070f271e5375dc42c6bb93b461ab1c0aa5841d4009267e0cfd95a39dca94a43", size = 8237244, upload-time = "2026-09-10T18:33:05.26Z" }, - { url = "https://files.pythonhosted.org/packages/9e/08/7a89bcdadd1fff0d464d01056417b646c9abcbc54f7297a0a1203bba5ebb/scikit_learn-1.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:613f0a783ca05aa844a4e1ac42d48425058f2c52be73f40f8cd98b7cd111acd6", size = 7875398, upload-time = "2026-09-10T18:33:07.506Z" }, - { url = "https://files.pythonhosted.org/packages/64/e3/b58e45082dcf3dcf0eb1192ee03545ec43d8c98441dfe20e88afca8442ce/scikit_learn-1.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5d117952769b563067656784e03c75a2d8235a7a05cf7fffa78a311e75aac08", size = 8747414, upload-time = "2026-09-10T18:33:10.698Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ed/d68115577c8b42b0442ebd8180945d4008880a33176094640e00b585128e/scikit_learn-1.9.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:8893bc6331f60f18d4ac75e12ed356e2dcf6a564bf767918b5b7ca54c8c8be49", size = 8279175, upload-time = "2026-09-10T18:33:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6c/06c7eb61a438e389cbf3f7210897069bec5a883dbe03c189ae792781e11a/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5492cf2df5226691c32611de8734bcf42148c6547ae53c7f4e6b847793addc0", size = 8881706, upload-time = "2026-09-10T18:33:14.741Z" }, - { url = "https://files.pythonhosted.org/packages/86/4e/0bab75490ca4b85fad8388739c7ebc71d9db553f8c69e39943ee8db0aaae/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993d332ff80e62efae9e39603b7e872297c418d780f01a01855269a3489c950f", size = 9152030, upload-time = "2026-09-10T18:33:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/9a/13/31c6f8ba1b7eecef9dd9558576c752d2ec5785fd0e456bb9fc59305be23f/scikit_learn-1.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:ca9051447455dae341d4d591eece7deb2d8e3d1020298fc87a81fc51e4da8f53", size = 8377024, upload-time = "2026-09-10T18:33:19.941Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/6d3cb8a45f5228f915acd66b819dd6b8232ccbe24532f51d278e3991df31/scikit_learn-1.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:90de6573f733a9fb79476ff1371af52a397d41c8b35f9146e20923db010d67b6", size = 8014196, upload-time = "2026-09-10T18:33:22.126Z" }, - { url = "https://files.pythonhosted.org/packages/66/6e/6befb2d5961490d18d9dbc16a5df37aa121d08bc9893316a6a363977a903/scikit_learn-1.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b5cad1624de8b75e5b9ccb7b0ce1ff1d01306340a3efc56d5529c5ba92392eb", size = 9066066, upload-time = "2026-09-10T18:33:24.396Z" }, - { url = "https://files.pythonhosted.org/packages/cb/18/11271f2f7db337db01f598e358721b1e83989407131272e5dd64214c28a8/scikit_learn-1.9.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:d137ce8a6142029fb5c35bd82f470c40cd9e760e5e2f7694b362c497c4ab3fa2", size = 8647654, upload-time = "2026-09-10T18:33:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/e7/04/9c15d201e1b6a2e81b8215865df7646c5a360f560831769c8dc92ac1ab9a/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66f852f7325b5070bc28329005aca76055a2def78faac039548ae889aeaa45a6", size = 8889070, upload-time = "2026-09-10T18:33:28.95Z" }, - { url = "https://files.pythonhosted.org/packages/1a/5a/4cb6c85160af4a639e87a3b7bf8b1c25cfc3b504c5af710ca416a6dcfc5f/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748bcb0a4cc04aec470652c9e5ec68450948e867387e7dfade647107ade68d25", size = 9131227, upload-time = "2026-09-10T18:33:31.008Z" }, - { url = "https://files.pythonhosted.org/packages/00/0e/361440972ae3d19b90ea88a84791138a51de0e8432a770ba741b2c8d9ced/scikit_learn-1.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:38cd925e893e5539be704d5edc64dbe081aacdab6b89d8c2977c1f6a7a453ce5", size = 8683001, upload-time = "2026-09-10T18:33:33.188Z" }, - { url = "https://files.pythonhosted.org/packages/e2/8f/a9f405c5c0e2df6f343a871b40c97fb32969e3ccc38e3033dd118f3c261e/scikit_learn-1.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b01e5b01735d38474127ca3f49319b592506225a87793b27559816b5c75cea39", size = 8257996, upload-time = "2026-09-10T18:33:35.343Z" }, - { url = "https://files.pythonhosted.org/packages/e5/c5/74a83ea39cef7cd07f53e06cc1cf51e79f35df74f81db956835d59ec34b1/scikit_learn-1.9.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8dec64f31a6e0ec826aca6c1b39a51e16d946e400d4c0904316f3ca72ccfb825", size = 8747991, upload-time = "2026-09-10T18:33:37.4Z" }, - { url = "https://files.pythonhosted.org/packages/64/c9/cc93e8a7fe204e43d70e96e5eb89871643be20025eea05eb4fdaf19afe39/scikit_learn-1.9.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e1b468241f4a7a9a7a0d6479ad3cc47681cc151a4046c530f2777c3d68f08942", size = 8279340, upload-time = "2026-09-10T18:33:39.705Z" }, - { url = "https://files.pythonhosted.org/packages/a2/61/0c6080f0d356fb966053009e7f25e9bff6cf74b0b16195ccf0c3757d1ae8/scikit_learn-1.9.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ca869d0080a5723cde2d5a8b54a2da1ff7e68735a9e9adb3da1243183a0fa01", size = 8881917, upload-time = "2026-09-10T18:33:41.912Z" }, - { url = "https://files.pythonhosted.org/packages/47/bb/98a31f10fffbd39edcc2f8bf4119b29652248bb110b7d45c84e68aa293ab/scikit_learn-1.9.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6754b7cabfc3df0b1f7b38f7a344f559bbae9d82f0ac5e3d48cccbd19fdcefdf", size = 9152805, upload-time = "2026-09-10T18:33:44.23Z" }, - { url = "https://files.pythonhosted.org/packages/f6/48/858ceff52213cfd97c0a069362071756bcb70b9fac9771b38d87c4cf7f17/scikit_learn-1.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:52cfdb1fed3a34362dbc0bd96f2e761a66fd5724d6901629f5a558f1f3bd9849", size = 8376719, upload-time = "2026-09-10T18:33:46.492Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1e/5337a871bdea53effbd154b61429df048f2665653251de74a3bd8a6dea9e/scikit_learn-1.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:ae6571a4828c6f5019bcd2b4125e5b18c0af3dbc9c99726c891f45f41335ec8e", size = 8014341, upload-time = "2026-09-10T18:33:48.762Z" }, - { url = "https://files.pythonhosted.org/packages/0e/35/150383a42d83ec4c7b39f9c50bd68408ecf04c19fc30ea5198fa42e67d9c/scikit_learn-1.9.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:48fefd8eb42bd4eec3e2d348149368ccd6d71987e20c30706a56a24eb86a6e73", size = 9059953, upload-time = "2026-09-10T18:33:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/9f/dd/aa0d738808540f7eaacfab93e01982db8ef1c1c7473ef0ad38193e6aebd1/scikit_learn-1.9.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:09f4d73049cd63575157f6b1060e06a8c83a4bd3488dbfaeedf35ccba7aad712", size = 8646602, upload-time = "2026-09-10T18:33:53.23Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/687ae4214c2f598906c3b9fa5f86fbaf834b35e20360625528ff1b713f06/scikit_learn-1.9.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3da53831534214322d9cb240fa6f390b36cf69eba727a6d4bd3238677630d70", size = 8880366, upload-time = "2026-09-10T18:33:55.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/03/82215cb78ad1c513a4498777571fb28444621ad26ef636287551767b7732/scikit_learn-1.9.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caae15634feceafa2612566b109a3082d3293167fac388eedaf77bff66b51983", size = 9129640, upload-time = "2026-09-10T18:33:58.186Z" }, - { url = "https://files.pythonhosted.org/packages/d4/90/4b7af4efd7909a4a0524a9f18457e4eb2eb60616eff2c7627eda8e3cdceb/scikit_learn-1.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ffbcbbbb44202fbe9bc64bced25a145759adb9ef010b3d37a8064958ac13df2a", size = 8678083, upload-time = "2026-09-10T18:34:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/31/27/068e484d4b83004302e0d9cfc1faca69bcc010d76fbb66a642446095af1b/scikit_learn-1.9.1-cp315-cp315t-win_arm64.whl", hash = "sha256:800dd22dd87fe97dcea484c24e85dd93cf1734d86bd74e668ad18f7967f4d1b5", size = 8259434, upload-time = "2026-09-10T18:34:02.678Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, ] [[package]] @@ -3986,6 +3989,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/18/489c97b834dfff9cf2fc2507cede4bcd4b11e67f84bc462acd1992496f86/structlog-26.1.0-py3-none-any.whl", hash = "sha256:e081a26d6c373e6d201eca24eede26d8ffab07f88f477822e679183428d3d91e", size = 73764, upload-time = "2026-06-06T07:33:38.046Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "syrupy" version = "6.1.1"