coderouter-cli 2.5.3__tar.gz → 2.5.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/CHANGELOG.md +44 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/PKG-INFO +1 -1
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/output_filters.py +148 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/pyproject.toml +1 -1
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_output_filters.py +6 -1
- coderouter_cli-2.5.4/tests/test_repair_byte_fallback.py +175 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/.gitignore +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/LICENSE +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/README.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/README.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/__main__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/adapters/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/adapters/anthropic_native.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/adapters/base.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/adapters/openai_compat.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/adapters/registry.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/cli.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/cli_stats.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/config/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/config/capability_registry.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/config/env_file.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/config/loader.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/config/schemas.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/cost.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/data/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/data/model-capabilities.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/doctor.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/doctor_apply.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/env_security.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/errors.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/gguf_introspect.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/_fingerprint.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/backend_health.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/context_budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/continuous_probe.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/drift_actions.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/drift_detection.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/memory_budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/memory_pressure.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/self_healing.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/guards/tool_loop.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/hardware.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/anthropic_routes.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/app.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/dashboard_routes.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/launcher_routes.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/metrics_routes.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/ingress/openai_routes.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/logging.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/metrics/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/metrics/collector.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/metrics/prometheus.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/plugins/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/plugins/base.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/plugins/loader.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/plugins/registry.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/adaptive.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/auto_router.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/capability.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/routing/fallback.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/audit_log.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/replay.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/request_log.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/store.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/state/suggest_rules.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/token_estimation.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/token_estimation_accurate.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/translation/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/translation/anthropic.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/translation/convert.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/coderouter/translation/tool_repair.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/README.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/assets/dashboard-demo.png +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/gguf_dl.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/hf-ollama-models.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/install-backends.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/install-backends.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/launcher-quickstart.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/launcher.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/llamacpp-direct.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/llamacpp-direct.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/lmstudio-direct.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/lmstudio-direct.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/backends/verify-ollama-0.23.1.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/concepts/architecture.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/concepts/context-budget.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/concepts/continuous-probing.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/concepts/drift-detection.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/concepts/partial-stitch.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/designs/v1.5-dashboard-mockup.html +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/designs/v1.6-auto-router-verification.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/designs/v1.6-auto-router.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/free-tier-guide.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/free-tier-guide.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/security.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/security.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/troubleshooting.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/troubleshooting.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/usage-guide.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/guides/usage-guide.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/low-memory-integration.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/openrouter-roster/CHANGES.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/openrouter-roster/README.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/openrouter-roster/latest.json +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v0.4.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v0.5-verify.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v0.5.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v0.6.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v0.7.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v1.0-verify.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/retrospectives/v1.0.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/start/quickstart.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/start/quickstart.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/start/when-do-i-need-coderouter.en.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/docs/start/when-do-i-need-coderouter.md +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/.env.example +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.auto-custom.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.auto.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.llama-cpp-vllm.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.note-2026.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.nvidia-nim.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.raspberrypi.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.v2-context-budget.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/examples/providers.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/demo_traffic.sh +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/smoke_v2_2.sh +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/verify-providers.yaml +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/verify_ollama_0_23.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/verify_v0_5.sh +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/scripts/verify_v1_0.sh +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/__init__.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/conftest.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_adapter_anthropic.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_audit_log.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_auto_router.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_backend_health.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_capability.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_capability_degraded_payload.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_capability_registry.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_capability_registry_cache_control.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_claude_code_suitability.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_cli.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_cli_stats.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_config.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_context_budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_continuous_probe.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_dashboard_endpoint.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_doctor.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_doctor_apply.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_doctor_cache_probe.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_drift_actions.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_drift_detection.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_drift_detection_integration.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_env_file.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_env_security.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_errors.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_examples_yaml.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_anthropic.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_cache_control.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_cache_observed.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_misconfig_warn.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_paid_gate.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_fallback_thinking.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_gguf_introspect.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_guards_tool_loop.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_hardware.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_ingress_anthropic.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_ingress_profile.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_memory_budget.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_memory_pressure.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_cache.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_collector.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_cost.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_endpoint.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_jsonl.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_prometheus.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_metrics_prometheus_cache.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_openai_compat.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_openrouter_roster_diff.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_output_filters_adapters.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_partial_stitch.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_plugins_integration.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_plugins_loader.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_plugins_registry.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_reasoning_strip.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_request_log.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_routing_adaptive.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_self_healing.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_setup_sh.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_state_store.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_token_estimation.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_token_estimation_accurate.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_tool_repair.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_translation_anthropic.py +0 -0
- {coderouter_cli-2.5.3 → coderouter_cli-2.5.4}/tests/test_translation_reverse.py +0 -0
|
@@ -6,6 +6,50 @@ versioning follows [SemVer](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [v2.5.4] — 2026-06-05 (Gemma `<0xNN>` byte-fallback repair filter)
|
|
10
|
+
|
|
11
|
+
Patch release: a new opt-in output filter that repairs Japanese (and other
|
|
12
|
+
multi-byte) text corrupted by the Ollama 0.30 / llama.cpp detokenizer change.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`repair_byte_fallback` output filter.** Ollama 0.30 unified its GGUF
|
|
17
|
+
runtime onto llama.cpp (`ollama/ollama#16031`). For gemma4 the detokenizer
|
|
18
|
+
changed, and multi-byte characters it cannot assemble now leak as
|
|
19
|
+
llama.cpp's byte-fallback notation `<0xNN>`:
|
|
20
|
+
- full-width space ` ` → `<0xE3><0x80><0x80>`
|
|
21
|
+
- rare kanji `躙` → `<0xE8><0xBA><0x99>`
|
|
22
|
+
|
|
23
|
+
These corrupt Japanese prose **and** tool-call JSON argument strings routed
|
|
24
|
+
through CodeRouter (a stray `<0xNN>` inside an argument breaks JSON parsing).
|
|
25
|
+
The new filter reassembles consecutive `<0xNN>` runs back into UTF-8.
|
|
26
|
+
|
|
27
|
+
- **Opt-in** per provider: `output_filters: [repair_byte_fallback]`
|
|
28
|
+
(disabled by default). Place it **before** `tool_repair` / tool-call XML
|
|
29
|
+
strip so byte-fallback inside tool-call arguments is restored before JSON
|
|
30
|
+
extraction.
|
|
31
|
+
- **Streaming-safe**: handles chunk boundaries inside a single token
|
|
32
|
+
(`<0x` | `E3>`) and inside a multi-byte run (`<0xE3>` | `<0x80><0x80>`).
|
|
33
|
+
A pending byte run is only flushed once it has definitively ended.
|
|
34
|
+
- **Lossless**: bytes that cannot form valid UTF-8 are re-emitted verbatim
|
|
35
|
+
as `<0xNN>`, so output is never made worse than llama.cpp already left it.
|
|
36
|
+
- **No new runtime dependencies** (stdlib `re` only).
|
|
37
|
+
|
|
38
|
+
Verified with 22 new unit tests (61 filter tests total pass on py3.12),
|
|
39
|
+
ruff clean, a 20,000-iteration streaming chunk-boundary fuzz (0 mismatches),
|
|
40
|
+
and a Japanese/emoji round-trip.
|
|
41
|
+
|
|
42
|
+
Ref: <https://note.com/akb428/n/n737e786f32ce>
|
|
43
|
+
|
|
44
|
+
### Known mitigations (documented)
|
|
45
|
+
|
|
46
|
+
- For gemma4, staying on / downgrading to Ollama 0.24 is the most reliable
|
|
47
|
+
fix (the root cause is the 0.30 llama.cpp detokenizer swap).
|
|
48
|
+
- A larger `num_ctx` increases the leak rate; consider not auto-raising it for
|
|
49
|
+
gemma4 on Ollama 0.30.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
9
53
|
## [v2.5.2] — 2026-05-22 (Backend-aware Launcher suggestions + backend install guide)
|
|
10
54
|
|
|
11
55
|
Patch release: a Launcher bug fix and documentation improvements.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -43,6 +43,7 @@ Reference: plan.md §10.2 "出力クリーニング" / docs/retrospectives/v0.7.
|
|
|
43
43
|
|
|
44
44
|
from __future__ import annotations
|
|
45
45
|
|
|
46
|
+
import re
|
|
46
47
|
from typing import Protocol
|
|
47
48
|
|
|
48
49
|
__all__ = [
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"KNOWN_FILTERS",
|
|
51
52
|
"OutputFilter",
|
|
52
53
|
"OutputFilterChain",
|
|
54
|
+
"RepairByteFallbackFilter",
|
|
53
55
|
"StripStopMarkersFilter",
|
|
54
56
|
"StripThinkingFilter",
|
|
55
57
|
"StripToolCallXmlFilter",
|
|
@@ -382,6 +384,151 @@ class StripToolCallXmlFilter:
|
|
|
382
384
|
return "".join(out_parts)
|
|
383
385
|
|
|
384
386
|
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
# repair_byte_fallback (v2.x)
|
|
389
|
+
# ---------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# A complete byte-fallback token: ``<0x`` + exactly two hex digits + ``>``.
|
|
393
|
+
_BYTE_RE = re.compile(r"<0x([0-9A-Fa-f]{2})>")
|
|
394
|
+
|
|
395
|
+
# The whole remaining buffer is a *proper prefix* of some ``<0xHH>`` token,
|
|
396
|
+
# i.e. it could still complete (and continue a run) on the next feed:
|
|
397
|
+
# ``<`` / ``<0`` / ``<0x`` / ``<0xH`` / ``<0xHH`` (closing ``>`` not yet seen).
|
|
398
|
+
_PREFIX_RE = re.compile(r"<(0(x[0-9A-Fa-f]{0,2})?)?")
|
|
399
|
+
|
|
400
|
+
_BYTE_TOKEN_START = "<0x"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _decode_byte_run(buf: bytes) -> str:
|
|
404
|
+
"""Decode a run of fallback bytes to text, losslessly.
|
|
405
|
+
|
|
406
|
+
Decodes the maximal valid UTF-8 prefix; any byte that cannot start or
|
|
407
|
+
continue a valid sequence is re-emitted as its ``<0xHH>`` token and
|
|
408
|
+
decoding resumes after it. So ``b"\\xe3\\x80\\x80"`` -> ``" "`` while a
|
|
409
|
+
stray ``b"\\xff"`` round-trips to ``"<0xFF>"`` — we never make the stream
|
|
410
|
+
worse than llama.cpp already did.
|
|
411
|
+
"""
|
|
412
|
+
parts: list[str] = []
|
|
413
|
+
i = 0
|
|
414
|
+
n = len(buf)
|
|
415
|
+
while i < n:
|
|
416
|
+
try:
|
|
417
|
+
parts.append(buf[i:].decode("utf-8"))
|
|
418
|
+
break
|
|
419
|
+
except UnicodeDecodeError as exc:
|
|
420
|
+
good_end = i + exc.start
|
|
421
|
+
if good_end > i:
|
|
422
|
+
parts.append(buf[i:good_end].decode("utf-8"))
|
|
423
|
+
parts.append(f"<0x{buf[good_end]:02X}>")
|
|
424
|
+
i = good_end + 1
|
|
425
|
+
return "".join(parts)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class RepairByteFallbackFilter:
|
|
429
|
+
"""Reassemble llama.cpp ``<0xNN>`` byte-fallback leaks into UTF-8 text.
|
|
430
|
+
|
|
431
|
+
Ollama 0.30 unified its GGUF runtime onto llama.cpp
|
|
432
|
+
(``ollama/ollama#16031``). For gemma4 the detokenizer changed, and
|
|
433
|
+
multi-byte characters it cannot assemble now leak as llama.cpp's
|
|
434
|
+
byte-fallback notation::
|
|
435
|
+
|
|
436
|
+
full-width space `` `` -> ``<0xE3><0x80><0x80>``
|
|
437
|
+
rare kanji ``躙`` -> ``<0xE8><0xBA><0x99>``
|
|
438
|
+
|
|
439
|
+
These corrupt Japanese prose AND tool-call JSON arguments (a stray
|
|
440
|
+
``<0xNN>`` inside an argument string breaks JSON parsing). This filter
|
|
441
|
+
reassembles runs of consecutive ``<0xNN>`` tokens back into UTF-8.
|
|
442
|
+
|
|
443
|
+
Stateful across ``feed`` calls so a token split across SSE deltas
|
|
444
|
+
(``<0x`` | ``E3>``) and a multi-byte run split across deltas
|
|
445
|
+
(``<0xE3>`` | ``<0x80><0x80>``) both reassemble correctly. A pending byte
|
|
446
|
+
run is only flushed once we are certain it has ended (confirmed normal
|
|
447
|
+
text follows, or ``eof``) — never at a bare chunk boundary, where the run
|
|
448
|
+
might continue in the next delta. Bytes that cannot form valid UTF-8 are
|
|
449
|
+
re-emitted verbatim (lossless).
|
|
450
|
+
|
|
451
|
+
``modified`` flips True the first time any ``<0xNN>`` token is consumed —
|
|
452
|
+
the adapter uses it to gate the log-once "output-filter-applied" line.
|
|
453
|
+
|
|
454
|
+
Ordering note: place this BEFORE ``tool_repair`` / the tool-call XML
|
|
455
|
+
strip so byte-fallback inside tool-call argument strings is restored
|
|
456
|
+
before JSON extraction.
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
name = "repair_byte_fallback"
|
|
460
|
+
|
|
461
|
+
def __init__(self) -> None:
|
|
462
|
+
"""Initialize per-request buffer, pending byte run and state."""
|
|
463
|
+
self.modified: bool = False
|
|
464
|
+
self._buffer: str = ""
|
|
465
|
+
self._pending = bytearray()
|
|
466
|
+
|
|
467
|
+
def _flush_pending(self, out: list[str]) -> None:
|
|
468
|
+
"""Decode and emit the accumulated byte run, then clear it."""
|
|
469
|
+
if self._pending:
|
|
470
|
+
out.append(_decode_byte_run(bytes(self._pending)))
|
|
471
|
+
self._pending.clear()
|
|
472
|
+
|
|
473
|
+
def feed(self, text: str, *, eof: bool = False) -> str:
|
|
474
|
+
"""Consume ``text``; return the portion safe to emit now."""
|
|
475
|
+
self._buffer += text
|
|
476
|
+
out: list[str] = []
|
|
477
|
+
|
|
478
|
+
while self._buffer:
|
|
479
|
+
m = _BYTE_RE.match(self._buffer)
|
|
480
|
+
if m is not None:
|
|
481
|
+
# Complete byte token at position 0 — extend the run.
|
|
482
|
+
self._pending.append(int(m.group(1), 16))
|
|
483
|
+
self._buffer = self._buffer[m.end() :]
|
|
484
|
+
self.modified = True
|
|
485
|
+
continue
|
|
486
|
+
|
|
487
|
+
idx = self._buffer.find(_BYTE_TOKEN_START)
|
|
488
|
+
if idx == -1:
|
|
489
|
+
# No complete/started token in the buffer. Hold a trailing
|
|
490
|
+
# partial of ``<0x`` (it may complete — and CONTINUE the run —
|
|
491
|
+
# on the next feed); treat anything before it as confirmed
|
|
492
|
+
# normal text that ends the run.
|
|
493
|
+
hold = (
|
|
494
|
+
0 if eof else _max_suffix_overlap(self._buffer, _BYTE_TOKEN_START)
|
|
495
|
+
)
|
|
496
|
+
safe = self._buffer[:-hold] if hold else self._buffer
|
|
497
|
+
if safe:
|
|
498
|
+
self._flush_pending(out)
|
|
499
|
+
out.append(safe)
|
|
500
|
+
self._buffer = self._buffer[len(safe) :]
|
|
501
|
+
# else: whole buffer is a token-start prefix; keep pending
|
|
502
|
+
# (the run might continue) and wait for more input.
|
|
503
|
+
break
|
|
504
|
+
|
|
505
|
+
if idx > 0:
|
|
506
|
+
# Normal text precedes the next token start — run ended.
|
|
507
|
+
self._flush_pending(out)
|
|
508
|
+
out.append(self._buffer[:idx])
|
|
509
|
+
self._buffer = self._buffer[idx:]
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
# idx == 0: buffer starts with ``<0x`` but is not a complete token.
|
|
513
|
+
if not eof and _PREFIX_RE.fullmatch(self._buffer):
|
|
514
|
+
# Could still complete next feed — hold token AND pending run.
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
# Malformed ``<0x..`` (non-hex, or stuck at eof). The ``<`` is
|
|
518
|
+
# ordinary text; the run (if any) has ended.
|
|
519
|
+
self._flush_pending(out)
|
|
520
|
+
out.append("<")
|
|
521
|
+
self._buffer = self._buffer[1:]
|
|
522
|
+
|
|
523
|
+
if eof:
|
|
524
|
+
self._flush_pending(out)
|
|
525
|
+
if self._buffer:
|
|
526
|
+
out.append(self._buffer)
|
|
527
|
+
self._buffer = ""
|
|
528
|
+
|
|
529
|
+
return "".join(out)
|
|
530
|
+
|
|
531
|
+
|
|
385
532
|
# ---------------------------------------------------------------------------
|
|
386
533
|
# Registry + chain
|
|
387
534
|
# ---------------------------------------------------------------------------
|
|
@@ -391,6 +538,7 @@ KNOWN_FILTERS: dict[str, type[OutputFilter]] = {
|
|
|
391
538
|
StripThinkingFilter.name: StripThinkingFilter,
|
|
392
539
|
StripStopMarkersFilter.name: StripStopMarkersFilter,
|
|
393
540
|
StripToolCallXmlFilter.name: StripToolCallXmlFilter,
|
|
541
|
+
RepairByteFallbackFilter.name: RepairByteFallbackFilter,
|
|
394
542
|
}
|
|
395
543
|
"""Registry of string-name → filter class.
|
|
396
544
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# in plan.md §11.B; once granted, this name will become an alias and
|
|
12
12
|
# `coderouter` will become the canonical distribution name.
|
|
13
13
|
name = "coderouter-cli"
|
|
14
|
-
version = "2.5.
|
|
14
|
+
version = "2.5.4"
|
|
15
15
|
description = "Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible."
|
|
16
16
|
readme = "README.md"
|
|
17
17
|
requires-python = ">=3.12"
|
|
@@ -61,7 +61,12 @@ def test_validate_rejects_when_mixing_known_and_unknown() -> None:
|
|
|
61
61
|
|
|
62
62
|
def test_registry_matches_expected_set() -> None:
|
|
63
63
|
"""Regression: if a filter is added, this test reminds us to doc it."""
|
|
64
|
-
assert set(KNOWN_FILTERS) == {
|
|
64
|
+
assert set(KNOWN_FILTERS) == {
|
|
65
|
+
"strip_thinking",
|
|
66
|
+
"strip_stop_markers",
|
|
67
|
+
"strip_tool_call_xml",
|
|
68
|
+
"repair_byte_fallback",
|
|
69
|
+
}
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
# ======================================================================
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Unit tests for ``repair_byte_fallback`` — the Gemma/llama.cpp
|
|
2
|
+
``<0xNN>`` byte-fallback repair filter.
|
|
3
|
+
|
|
4
|
+
Mirrors the streaming-boundary style of ``test_output_filters.py``: we
|
|
5
|
+
exercise (1) whole-string repair, (2) chunk boundaries inside a single
|
|
6
|
+
token, (3) chunk boundaries inside a multi-byte run, (4) passthrough of
|
|
7
|
+
ordinary text, and (5) lossless handling of malformed/incomplete tokens.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from coderouter.output_filters import (
|
|
15
|
+
KNOWN_FILTERS,
|
|
16
|
+
RepairByteFallbackFilter,
|
|
17
|
+
apply_output_filters,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _repair(text: str) -> tuple[str, bool]:
|
|
22
|
+
"""One-shot repair, returning ``(out, modified)``."""
|
|
23
|
+
f = RepairByteFallbackFilter()
|
|
24
|
+
out = f.feed(text, eof=True)
|
|
25
|
+
return out, f.modified
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _stream(chunks: list[str]) -> str:
|
|
29
|
+
"""Feed chunks one at a time, flush at eof, return concatenated output."""
|
|
30
|
+
f = RepairByteFallbackFilter()
|
|
31
|
+
out = []
|
|
32
|
+
for c in chunks:
|
|
33
|
+
out.append(f.feed(c))
|
|
34
|
+
out.append(f.feed("", eof=True))
|
|
35
|
+
return "".join(out)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ----------------------------------------------------------------------
|
|
39
|
+
# registry wiring
|
|
40
|
+
# ----------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_registered_in_known_filters() -> None:
|
|
44
|
+
assert KNOWN_FILTERS["repair_byte_fallback"] is RepairByteFallbackFilter
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_usable_through_chain() -> None:
|
|
48
|
+
out, applied = apply_output_filters(
|
|
49
|
+
["repair_byte_fallback"], "残酷な<0xE3><0x80><0x80>蹂"
|
|
50
|
+
)
|
|
51
|
+
assert out == "残酷な 蹂"
|
|
52
|
+
assert applied == ["repair_byte_fallback"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ----------------------------------------------------------------------
|
|
56
|
+
# 1. whole-string repair
|
|
57
|
+
# ----------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_full_width_space_single_shot() -> None:
|
|
61
|
+
out, mod = _repair("残酷な<0xE3><0x80><0x80>蹂")
|
|
62
|
+
assert out == "残酷な 蹂"
|
|
63
|
+
assert mod is True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_rare_kanji_single_shot() -> None:
|
|
67
|
+
# 躙 = U+8E99 = E8 BA 99
|
|
68
|
+
out, mod = _repair("残酷な蹂<0xE8><0xBA><0x99>だった")
|
|
69
|
+
assert out == "残酷な蹂躙だった"
|
|
70
|
+
assert mod is True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_mixed_multiple_runs() -> None:
|
|
74
|
+
out, _ = _repair("A<0xE3><0x80><0x80>B<0xE8><0xBA><0x99>C")
|
|
75
|
+
assert out == "A B躙C"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ----------------------------------------------------------------------
|
|
79
|
+
# 2. chunk boundary inside a single token
|
|
80
|
+
# ----------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_split_inside_token() -> None:
|
|
84
|
+
assert _stream(["残酷な<0x", "E3><0x80><0x80>蹂"]) == "残酷な 蹂"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_split_every_char() -> None:
|
|
88
|
+
s = "x<0xE3><0x80><0x80>y"
|
|
89
|
+
assert _stream(list(s)) == "x y"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ----------------------------------------------------------------------
|
|
93
|
+
# 3. chunk boundary inside a multi-byte run
|
|
94
|
+
# ----------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_split_between_run_tokens() -> None:
|
|
98
|
+
assert _stream(["<0xE3>", "<0x80><0x80>"]) == " "
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_split_run_three_ways() -> None:
|
|
102
|
+
assert _stream(["pre<0xE8>", "<0xBA>", "<0x99>post"]) == "pre躙post"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ----------------------------------------------------------------------
|
|
106
|
+
# 4. ordinary text passthrough (must not corrupt existing content)
|
|
107
|
+
# ----------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_plain_text_untouched() -> None:
|
|
111
|
+
out, mod = _repair("def foo(): return 1 # 普通の日本語")
|
|
112
|
+
assert out == "def foo(): return 1 # 普通の日本語"
|
|
113
|
+
assert mod is False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_non_byte_angle_tags_untouched() -> None:
|
|
117
|
+
s = "<think>hmm</think><tool_call>{}</tool_call>"
|
|
118
|
+
out, mod = _repair(s)
|
|
119
|
+
assert out == s
|
|
120
|
+
assert mod is False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_literal_less_than_zero_passes() -> None:
|
|
124
|
+
out, _ = _repair("if x <0 and y >1: pass")
|
|
125
|
+
assert out == "if x <0 and y >1: pass"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ----------------------------------------------------------------------
|
|
129
|
+
# 5. lossless handling of malformed / incomplete tokens
|
|
130
|
+
# ----------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_malformed_non_hex_kept_literal() -> None:
|
|
134
|
+
out, mod = _repair("a<0xZZ>b")
|
|
135
|
+
assert out == "a<0xZZ>b"
|
|
136
|
+
assert mod is False
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_incomplete_token_at_eof_kept_literal() -> None:
|
|
140
|
+
out, _ = _repair("tail<0xE3")
|
|
141
|
+
assert out == "tail<0xE3"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_lone_invalid_byte_round_trips() -> None:
|
|
145
|
+
out, mod = _repair("x<0xFF>y")
|
|
146
|
+
assert out == "x<0xFF>y"
|
|
147
|
+
assert mod is True # a token WAS consumed, even though the byte was undecodable
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_valid_prefix_then_invalid_byte() -> None:
|
|
151
|
+
out, _ = _repair("<0xE3><0x80><0xFF>")
|
|
152
|
+
assert out == "<0xE3><0x80><0xFF>"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ----------------------------------------------------------------------
|
|
156
|
+
# invariants
|
|
157
|
+
# ----------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize(
|
|
161
|
+
"s",
|
|
162
|
+
["", "hello world", "マルチバイト日本語テキスト", "<<<>>>", "a < b and c > d"],
|
|
163
|
+
)
|
|
164
|
+
def test_no_byte_tokens_means_byte_identical(s: str) -> None:
|
|
165
|
+
out, mod = _repair(s)
|
|
166
|
+
assert out == s
|
|
167
|
+
assert mod is False
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_modified_flag_semantics() -> None:
|
|
171
|
+
f = RepairByteFallbackFilter()
|
|
172
|
+
f.feed("plain", eof=False)
|
|
173
|
+
assert f.modified is False
|
|
174
|
+
f.feed("<0xE3><0x80><0x80>", eof=True)
|
|
175
|
+
assert f.modified is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|