Codex routing calibration and evaluation#
Wersja polska: CODEX_EVAL_HOWTO_PL.md.
1. What you are actually calibrating#
This guide primarily covers agentic_routing_codex and the implementation in this repository described during the October 6–7, 2026 verification. The values and results below are reference points from that review, not a guarantee that they remain current after subsequent configuration or code changes.
The goal is to identify the current work mode correctly, and only then select the model assigned to it. You are not tuning routing around the name of a particular response-generating model.
There are three independent mechanisms:
- Structural rules and work phase — an explicit mode, Plan Mode, titles, compaction, tool calls, and their results.
- Heuristics — scoring keywords, phrases, and regular expressions.
- Semantics — embedding similarity between the current activity and mode descriptions and examples.
Optional phase memory in Redis complements these mechanisms.
Resolution order:
explicit mode
→ request class: title / compaction
→ Plan Mode declaration
→ current phase / trustworthy phase memory
→ heuristics
→ embeddings
→ fallback
→ mode-to-model mapping
Important: if heuristics already make an incorrect decision, lowering the embedding threshold will not fix it. The semantic layer is not consulted in that case.
Calibration in the current system does not mean training or fine-tuning the embedding model. It means selecting an embedding model, descriptions, examples, indexing parameters, and acceptance thresholds.
2. What the configuration files and settings are for#
| File / setting | Purpose |
|---|---|
llm_router_plugins/resources/routing/agentic_routing_codex.json |
Codex modes, rules, semantic examples, thresholds, and mode-to-model mappings. |
tests/data/codex_routing_quality.json |
Requests with expected modes. Used for evaluation; not automatically added to the embedding index. |
tests/data/codex_routing_baseline.json |
Frozen earlier predictions for comparisons on common cases. |
codex-routing-holdout-verification.json |
Saved output from a particular replay. A report, not configuration. |
LLM_ROUTER_MODELS_CONFIG |
Model configuration in the router application: availability, provider, serving parameters, etc. Does not define examples for work-mode classification. |
simple_semantic.json |
Configuration for a separate heuristic plugin. Despite its name, it does not require embeddings. |
semantic_biencoder.json |
Configuration for a separate embedding-based general routing plugin. Does not replace the Codex configuration. |
agentic_routing_claude_code.json |
Configuration for a different plugin, for Claude Code. |
Changing semantic_biencoder.json does not automatically calibrate Codex. The embedding engine is shared, but the configurations and decision layers are separate.
Key Codex configuration fields#
| Field | Meaning |
|---|---|
embedding_model |
The model that creates vectors; a local path or model identifier. Not the model answering the user. |
settings.trigger_model |
The alias that activates the plugin, auto_codex by default. |
settings.fallback_mode |
Mode selected when no mechanism resolves the request, currently implement. |
codex_modes[].name |
Mode name, such as review. |
codex_modes[].model_name |
Model used after this mode is selected. |
codex_modes[].description |
Description of the mode's meaning, indexed semantically. |
codex_modes[].examples |
Examples of activities belonging to the mode, indexed semantically. |
keywords, phrases, patterns, weights |
Heuristic signals, not embedding examples. |
settings.phase |
Activity interpretation rules: commands, tools, test paths, announcements, etc. |
settings.semantic |
Indexing and semantic acceptance parameters. |
settings.vector_store_path |
Optional directory for a persistent semantic index. Not session memory. |
Redis is a separate mechanism. Configure its connection using independent environment variables, not by adding connection details to the embedding configuration or automatically reusing Redis settings for auth.
3. Understanding scores and similarity#
Heuristic scoring#
Current default weights:
keyword = 1
phrase = 2
pattern = 3
You can change them through settings.heuristic_weights, and override the weight of an individual keyword or phrase.
Example syntax for fields within one mode:
{
"keywords": ["pytest"],
"weights": {"pytest": 3},
"phrases": ["run unit tests:4"],
"patterns": ["\\brun\\s+tests\\b"]
}
Overlapping matches are deduplicated: do not assume every matching rule always adds to the total. Repeating a single word does not increase the score indefinitely either.
Acceptance requires:
best score > 0
best score >= heuristic_min_score
best score > second score
best score - second score >= heuristic_min_margin
Current configuration:
heuristic_min_score = 3.0
heuristic_min_margin = 1.0
For heuristics, routing.similarity is a transformation:
similarity = score / (score + 1)
score = 3 → similarity = 0.75
This does not mean a “75% chance that the mode is correct.”
Embedding scores#
Descriptions and examples are converted into normalized vectors. The engine compares them with the request vector using cosine similarity.
With aggregation: "per_target_top_k":
- Similarities are retrieved for all indexed chunks.
- The same number of best chunks is selected for each mode.
- Their similarities are averaged.
- This produces the
all_scoresmode ranking.
The effective number of chunks per mode is:
min(top_k, number of chunks in the smallest class)
Therefore, top_k: 4 does not mean four modes in the result. It means up to four best chunks for each mode.
Acceptance requires:
s1 >= threshold
s1 > s2
s1 - s2 >= min_margin
Here, s1 is the best mode's score and s2 is the second-best score.
Current settings:
{
"threshold": 0.44,
"min_margin": 0.005,
"aggregation": "per_target_top_k"
}
Examples:
| Best score | Second score | Decision with the current thresholds |
|---|---|---|
0.62 |
0.54 |
Accept: sufficient score and margin. |
0.62 |
0.615 |
Reject: insufficient margin. |
0.40 |
0.35 |
Reject: score too low. |
0.62 |
0.62 |
Reject: tie. |
A cosine similarity of 0.62 does not mean a 62% probability of correctness either. Do not compare it directly with heuristic 0.75 or structural 1.0.
4. What actually goes into embeddings#
The mode index contains:
- the mode name and its
description; - texts from
examples.
Names of assigned response-generating models are not semantic evidence. aux_title and compaction are resolved structurally and do not participate in the main-mode semantic ranking.
The semantic query includes separate sections for:
- the intent of the current user instruction;
- current phase context: the agent's latest message and a structured description of tool activity.
Raw contents of a read file or tool result are not added as the phase description. This prevents the file's subject matter from replacing information about what the agent is doing.
Intent and phase sections are embedded separately, then their vectors are averaged and normalized.
| Parameter | What it controls |
|---|---|
intent_max_chars |
Character limit for the intent section; currently 2000. |
phase_max_chars |
Character limit for the phase section; currently 2000. |
classify_max_chars |
Separate history-parser budget; not an intent-versus-phase weight. |
chunk_size |
Chunk size for indexed descriptions/examples; currently 256. |
chunk_overlap |
Chunk overlap; currently 64. |
top_k |
Number of best chunks considered per mode; currently 4. |
Increasing phase_max_chars does not directly mean “give the phase twice the weight.” It changes how much text is available.
5. Preparing a good calibration dataset#
You already have tests/data/codex_routing_quality.json. Use it as a starting point, but stronger evaluation requires more independent sessions.
Minimal example of a separate dataset:
{
"schema_version": 1,
"cases": [
{
"id": "cal-review-001",
"split": "calibration",
"source_session": "session-cal-001",
"expected_mode": "review",
"ambiguous": false,
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Review this module's error handling and describe the issues. Do not change files."
}
]
}
]
},
{
"id": "holdout-review-001",
"split": "holdout",
"source_session": "session-holdout-001",
"expected_mode": "review",
"ambiguous": false,
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Assess the validator's correctness and identify risks without preparing a fix."
}
]
}
]
}
]
}
For phase sequences, add:
- a shared
sequencevalue; session_id,thread_id,agent_name, andturn_idin the case'smetadatafield;- actual history prefixes: tool calls, identifiers, and results;
- optional
payloadfor other request fields, such astools, response format, and Codex metadata.
Note: during replay, the case's metadata is added to the request's client_metadata. An explicit mode override can be retained in payload.
Labeling rules:
- The expected mode must follow solely from information available before the decision.
- A label describes the current activity, not the overall task subject.
- A historical router decision is not automatically a correct label.
- A complete session belongs to either
calibrationorholdout— do not split nearly identical prefixes across the two sets. - Mark genuinely unresolved cases as
ambiguous: true; do not use this to hide errors. - Remove secrets, but preserve the structure needed to recognize the phase.
Include difficult contrasts, such as:
- adding a mock in production code versus writing tests;
- GitHub integration versus Git history analysis;
- developing a testing strategy versus running tests;
- assessing code versus preparing a fix;
- a neutral file read after running tests;
- the phase sequence
implement → test → debug → implement.
6. Running evaluate#
Automated tuning and evaluation#
The Bash script runs the complete workflow (requires jq, Python with the project and [ml] dependencies):
bash scripts/codex-tune-eval.sh \
--python /path/to/venv/bin/python \
--thresholds "0.40 0.44 0.48 0.52 0.56" \
--margins "0.001 0.005 0.02 0.05" \
--output-dir ./workdir/codex-tuning-01
Use --config, --dataset and --baseline for custom files; --help lists all options. The output directory must be new. Without --output-dir, a new codex-eval-* directory is created in the repository. User-supplied paths are relative to the invocation directory; the script can run from any directory.
The script evaluates the source configuration, then every threshold/min_margin pair on calibration only. It selects the highest cascade.main_mode_accuracy, then fewer missed and unnecessary mode switches, then higher mode_accuracy. An exact tie retains the source configuration. Only after freezing selection does it evaluate source and selected configurations on holdout. It does not train embeddings, improve examples or tune heuristics.
Outputs include selection.json (candidate ranking), selected-config.json, holdout-comparison.json, full reports and separate logs for every replay. Source configuration and dataset snapshots are retained. The production configuration is never overwritten and the winner is not deployed automatically — inspect precision/recall, special cases and holdout regressions before deployment.
Each candidate performs a real replay and reloads the embedding model, so the default grid can be expensive. Errors stop the workflow; logs and partial reports remain for diagnosis. Use --no-semantic for evaluation without embeddings: it skips tuning and reports deterministic variants on both splits. The script neither installs dependencies nor uses production Redis.
Run the following commands from the repository root. python should point to a stable interpreter in the project's PyCharm environment.
Semantic evaluation requires optional dependencies:
python -m pip install -e '.[ml]'
This is an installation instruction for you, not a record of an installation performed. Earlier semantic verification was blocked by an environment using Python 3.11.0rc1; use a compatible, stable interpreter for these measurements.
A. Reference run without embeddings#
python -m llm_router_plugins.utils.routing.agentic_routing.codex.evaluation \
--config llm_router_plugins/resources/routing/agentic_routing_codex.json \
--dataset tests/data/codex_routing_quality.json \
--split calibration \
--no-semantic \
> codex-calibration-deterministic.json
This produces the deterministic and stateful variants.
B. Embedding evaluation on calibration#
python -m llm_router_plugins.utils.routing.agentic_routing.codex.evaluation \
--config llm_router_plugins/resources/routing/agentic_routing_codex.json \
--dataset tests/data/codex_routing_quality.json \
--split calibration \
> codex-calibration-semantic.json
This evaluation adds cascade and semantic_only.
C. Final holdout evaluation#
After tuning is complete:
python -m llm_router_plugins.utils.routing.agentic_routing.codex.evaluation \
--config llm_router_plugins/resources/routing/agentic_routing_codex.json \
--dataset tests/data/codex_routing_quality.json \
--split holdout \
--baseline tests/data/codex_routing_baseline.json \
> codex-holdout-semantic.json
--split holdout is the default, but specifying it explicitly is helpful. Use --split all for overall diagnostics, not for choosing thresholds using the control set.
Important evaluator behavior#
- It reads the specified
JSONfile; it does not apply the normal routing configuration overrides from environment variables. - It builds a fresh in-memory index; it does not use the production persistent index.
- It does not call response-generating models.
- It does not use production Redis. Memory is replayed in an isolated local store per sequence.
- Without
--no-semantic, it requires enabled semantic routing andaggregation: "per_target_top_k". - An embedding loading or lookup failure should abort evaluation rather than masquerade as a successful fallback evaluation.
Therefore, setting a threshold through an environment variable does not change this CLI's result. For an experiment, write the threshold into the file passed through --config.
7. Reading report variants#
| Variant | What it measures |
|---|---|
deterministic |
The cascade without embeddings or session memory. |
stateful |
The deterministic cascade with phase-memory replay. |
cascade |
The cascade with embeddings but without session memory. |
semantic_only |
Embeddings for main requests; titles and compaction still use structural routing. |
The current evaluator does not report a separate “memory + semantics” variant. Do not treat cascade as a complete production simulation with Redis enabled.
semantic_only helps reveal ranking quality itself. It is not the “maximum possible result,” because it deliberately bypasses some stronger structural signals.
Key metrics#
| Metric | Interpretation |
|---|---|
mode_accuracy |
Fraction of correct modes among labeled cases. |
main_mode_accuracy |
Accuracy for main work, excluding titles and compaction. |
per_mode.precision |
Of the predictions of a particular mode, how many were correct? |
per_mode.recall |
Of the cases requiring a particular mode, how many were detected? |
mode_confusion |
Row: expected mode; column: predicted mode. |
sources |
Which layer made the decisions. |
fallback_reasons |
Reasons for fallback decisions recorded by the variant. |
semantic_acceptance_rate |
Fraction of labeled cases resolved by the semantic source; not semantic precision or the acceptance rate restricted to lookups. |
special_cases |
Separate results for titles and compaction. |
ambiguous_count |
Cases excluded from accuracy because of ambiguity. |
mean_routing_ms |
Mean replay time for a variant, not total production latency. |
For example, high test recall and low test precision mean tests are detected, but this mode also captures unrelated tasks.
For sequences, pay particular attention to:
unnecessary_switches— a mode change when the expected phase did not change;missed_switches— no change when a transition was expected;mean_switch_delay— delay in detecting a new phase, measured in subsequent requests, not milliseconds;censored_switches— transitions for which the correct phase was not reached before the available fragment ended.
Correct switch counts alone do not prove correct mode selection. Always read them together with accuracy and the confusion matrix.
Do not select a configuration based on model_accuracy. If multiple modes share one model, an incorrect mode may look like a correct model selection.
8. A practical embedding calibration procedure#
Step 1: freeze a reference point#
Save a report for the current configuration on calibration. Preserve the configuration and dataset used for the measurement.
The report includes their hashes in metadata. Also record the code version, embedding weights revision, and dependency versions — a configuration hash cannot detect replacement of model files at the same path.
Step 2: identify where errors occur#
Start by comparing deterministic with cascade.
- Error from
heuristic→ improve the heuristic. - Error from
phase→ inspect activity rules and the parser. - Error from
memory→ inspect metadata and phase lifecycle. - Semantic error or unresolved fallback → inspect the embedding ranking.
Example for reading rankings if jq is available:
jq '.records[]
| select(.ambiguous == false)
| select(.semantic_only.all_scores != null)
| {
id,
expected: .expected_mode,
selected: .semantic_only.mode,
source: .semantic_only.source,
margin: .semantic_only.margin,
ranking: .semantic_only.all_scores
}' codex-calibration-semantic.json
Do not assess only the final mode: implement may be correctly selected as a fallback even though semantics accepted no class.
Step 3: improve descriptions and examples#
Good examples describe the activity and the mode's boundaries, not the repository's subject matter.
Example distinctions:
plan:
“Develop a testing strategy; do not write tests yet.”
test:
“Add unit tests for the validator and run them.”
review:
“Assess the validator's correctness and describe issues without editing.”
implement:
“Add input validation to the existing module.”
Recommendations:
- use representative Polish and English phrasing;
- add varied paraphrases, not dozens of nearly identical sentences;
- explain distinctions in
description; - do not use preferred model names as classification clues;
- do not copy
holdoutcases intoexamples; - avoid copying evaluation prompts verbatim even from calibration — use them to identify missing categories of phrasing.
If the correct class regularly ranks second, lowering threshold alone usually will not help: the wrong class will still win.
Step 4: select threshold and margin#
Once descriptions are stable, test a small grid of values, for example:
threshold: 0.40, 0.44, 0.48, 0.52, 0.56
min_margin: 0.001, 0.005, 0.02, 0.05
This is an example experimental range, not a recommended optimum. A different embedding model may require a different range.
- Higher
threshold: usually fewer acceptances and more fallbacks. - Lower
threshold: more acceptances, but potentially incorrect confident decisions. - Higher
min_margin: rejects competing classes with similar scores. - Lower
min_margin: accepts more borderline cases.
Example of creating a candidate configuration:
jq '.settings.semantic.threshold = 0.55
| .settings.semantic.min_margin = 0.08' \
llm_router_plugins/resources/routing/agentic_routing_codex.json \
> codex-candidate.json
Then:
python -m llm_router_plugins.utils.routing.agentic_routing.codex.evaluation \
--config codex-candidate.json \
--dataset tests/data/codex_routing_quality.json \
--split calibration \
> codex-candidate-calibration.json
The CLI has no separate --threshold or --min-margin options and no automatic tuning. Supply parameters through configuration.
Cost optimization: when only thresholds change, the all_scores ranking remains the same. You can screen threshold pairs offline using saved rankings instead of loading the model every time. However, confirm the selected configuration with an actual replay of the complete cascade.
Step 5: tune other parameters only afterward#
Check these separately:
top_k, such as1,3,5;- intent and phase section lengths;
chunk_sizeandchunk_overlap;- other embedding models.
Do not change everything at once — you will lose track of what caused an improvement.
After changing the embedding model, calibrate thresholds again. The same cosine value need not have the same diagnostic meaning for different models.
Step 6: select configuration by quality, not acceptance count#
Priorities:
- Fewer incorrect modes in main work.
- Fewer unnecessary and missed switches.
- No degradation in particularly important classes.
- No regressions in explicit overrides, Plan Mode, titles, or compaction.
- Acceptable routing cost and latency.
Fewer fallbacks do not automatically mean better routing. An incorrect semantic decision may be worse than a cautious fallback.
9. Calibrating heuristics#
Tune heuristics separately, starting with a --no-semantic report.
Common actions:
- remove overly broad keywords that capture unrelated activities;
- add more unambiguous phrases;
- reduce the weight of an ambiguous signal;
- increase
heuristic_min_marginif competing classes have similar scores; - check local negations, such as “do not run tests”;
- account for Polish spelling both with and without diacritics.
CodexModeScorer.rank_modes() exposes a ranking with raw scores and SignalMatch matches. This is the right place to investigate “which word added points.” The CLI report does not contain a complete heuristic ranking.
Not all configured modes are heuristic candidates. In particular, merely adding keywords to plan does not make it a heuristic winner in the current cascade; planning has structural signals and a semantic path.
After improving heuristics, rerun the embedding evaluation too — changing an earlier layer changes the requests that reach semantics.
10. Confirming results and deploying configuration#
After choosing a candidate on calibration, run the old and new configurations on the same independent holdout. Compare not just summaries, but individual lost and recovered cases.
If you change configuration to address errors after inspecting the holdout, that dataset is no longer an independent check. Keep it as regression coverage and prepare a new holdout.
The current corpus is small and has already been analyzed while implementing fixes. It is useful for regressions, but is not strong evidence of quality on new sessions.
Deployment#
Example of selecting a ready configuration file:
export LLM_ROUTER_ROUTING_SEMANTIC_AGENTIC_CODEX_CONFIG=/path/to/codex-candidate.json
Selected production overrides:
export LLM_ROUTER_ROUTING_SEMANTIC_AGENTIC_CODEX_SEMANTIC_ENABLED=true
export LLM_ROUTER_ROUTING_SEMANTIC_AGENTIC_CODEX_SIMILARITY_THRESHOLD=0.55
export LLM_ROUTER_ROUTING_SEMANTIC_AGENTIC_CODEX_MODEL=/path/to/embedding-model
Here, MODEL means the embedding model. MODEL_TEST, MODEL_REVIEW, etc. mean response-generating models assigned to modes.
semantic.min_margin, aggregation, and section budgets currently have no individual environment variable overrides; set them in JSON.
Changes are loaded when the plugin is created, so they require recreating the plugin / restarting workers.
Persistent index#
If you use vector_store_path or PERSIST_DIR, after changing:
- the embedding model;
- the set of modes;
- descriptions or examples;
- text chunking parameters;
build a new index. The safest approach is to specify a new, versioned directory. The current engine does not guarantee automatic index invalidation after every configuration change.
Changing only threshold, min_margin, top_k, or query budgets does not require re-embedding descriptions.
The FAISS index and Redis memory are independent. Do not clear all of Redis when calibrating embeddings.
11. What the current report tells you#
In codex-routing-holdout-verification.json from the earlier verification:
| Metric | deterministic |
stateful |
|---|---|---|
| Mode accuracy | 26/35 = 74.3% |
29/35 = 82.9% |
| Main-work accuracy | 23/32 = 71.9% |
26/32 = 81.25% |
| Unnecessary switches | 2 |
0 |
| Missed switches | 1 |
0 |
review recall |
25% |
25% |
plan recall |
33.3% |
33.3% |
This demonstrates an improvement from memory on these cases. It does not demonstrate embedding quality — neither variant uses embeddings, and semantic_acceptance_rate is 0.
Low review and plan recall identifies areas to investigate, not an automatic reason to lower the threshold. First check whether the incorrect mode comes from a heuristic or whether the correct mode ranks highly in the semantic ranking.
The current baseline also has unverified historical provenance. Use it as a frozen regression snapshot, not as evidence of results “before the entire implementation.” --baseline compares only common cases with consistent labels.
12. Short workflow checklist#
1. Check that the interpreter and embedding model work.
2. Prepare calibration and holdout sets separated by session.
3. Save results for the current configuration without semantics.
4. Save results for the current configuration with semantics.
5. Identify the layer responsible for errors.
6. Improve descriptions / examples or the relevant layer's rules.
7. Tune threshold and min_margin on calibration.
8. Check incorrect modes, precision/recall, and switches.
9. Confirm the selected candidate on an independent holdout.
10. Deploy configuration and a fresh index, then inspect production logs.
The key rule: optimize correctness of the current work mode, not high similarity, low fallback usage, or model-name accuracy.