lynkr 9.14.16 → 9.14.18
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.
- package/README.md +2 -0
- package/bin/cli.js +2 -0
- package/bin/lynkr-audit.js +58 -0
- package/bin/lynkr-route.js +76 -0
- package/config/complexity-exemplars.json +366 -0
- package/config/routing.example.json +186 -0
- package/config/routing.json +93 -0
- package/package.json +2 -2
- package/scripts/build-complexity-exemplars.js +84 -0
- package/scripts/build-routing-corpus.js +55 -0
- package/scripts/cache-probe.js +66 -0
- package/scripts/calibrate-capabilities.js +433 -0
- package/scripts/eval-complexity-signal.js +65 -0
- package/scripts/validate-grounding.js +63 -0
- package/src/api/router.js +53 -4
- package/src/cache/embeddings.js +22 -3
- package/src/clients/cursor-utils.js +1 -1
- package/src/clients/databricks.js +258 -124
- package/src/context/output-format-guard.js +1 -0
- package/src/orchestrator/index.js +11 -2
- package/src/routing/agentic-detector.js +6 -1
- package/src/routing/client-profiles.js +22 -0
- package/src/routing/complexity-analyzer.js +7 -0
- package/src/routing/decisions.js +100 -0
- package/src/routing/grounding.js +118 -0
- package/src/routing/harness-envelope.js +71 -1
- package/src/routing/index.js +104 -7
- package/src/routing/intent-score.js +5 -0
- package/src/routing/outcomes.js +136 -0
- package/src/routing/routing-config.js +154 -0
- package/src/routing/shortfall.js +90 -1
- package/src/routing/signals.js +179 -0
- package/src/routing/switch-gate.js +90 -0
- package/src/routing/telemetry.js +66 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"mode": "observe",
|
|
4
|
+
"signals": {
|
|
5
|
+
"anchor": {
|
|
6
|
+
"type": "anchor_score"
|
|
7
|
+
},
|
|
8
|
+
"judge": {
|
|
9
|
+
"type": "jev",
|
|
10
|
+
"min_confidence": 0.5
|
|
11
|
+
},
|
|
12
|
+
"harness": {
|
|
13
|
+
"type": "harness"
|
|
14
|
+
},
|
|
15
|
+
"structured": {
|
|
16
|
+
"type": "request_field",
|
|
17
|
+
"any_of": [
|
|
18
|
+
"output_format",
|
|
19
|
+
"response_format"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"phase": {
|
|
23
|
+
"type": "session_phase"
|
|
24
|
+
},
|
|
25
|
+
"prev": {
|
|
26
|
+
"type": "prev_outcome"
|
|
27
|
+
},
|
|
28
|
+
"forensics": {
|
|
29
|
+
"type": "keyword",
|
|
30
|
+
"terms": [
|
|
31
|
+
"recover",
|
|
32
|
+
"deleted",
|
|
33
|
+
"disk image",
|
|
34
|
+
"forensic",
|
|
35
|
+
"carve"
|
|
36
|
+
],
|
|
37
|
+
"min_hits": 1
|
|
38
|
+
},
|
|
39
|
+
"complexity": {
|
|
40
|
+
"type": "complexity",
|
|
41
|
+
"threshold": 0.6,
|
|
42
|
+
"scale": 0.15
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"decisions": [
|
|
46
|
+
{
|
|
47
|
+
"name": "harness_regressing_escalate",
|
|
48
|
+
"priority": 300,
|
|
49
|
+
"rules": {
|
|
50
|
+
"operator": "AND",
|
|
51
|
+
"conditions": [
|
|
52
|
+
{
|
|
53
|
+
"signal": "harness"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"signal": "prev",
|
|
57
|
+
"in": [
|
|
58
|
+
"regression",
|
|
59
|
+
"no_progress"
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"signal": "prev",
|
|
64
|
+
"min": 0
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"tier": "COMPLEX",
|
|
69
|
+
"effort": "medium"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "harness_hard_task",
|
|
73
|
+
"priority": 200,
|
|
74
|
+
"rules": {
|
|
75
|
+
"operator": "AND",
|
|
76
|
+
"conditions": [
|
|
77
|
+
{
|
|
78
|
+
"signal": "harness"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"operator": "OR",
|
|
82
|
+
"conditions": [
|
|
83
|
+
{
|
|
84
|
+
"signal": "judge",
|
|
85
|
+
"tier_in": [
|
|
86
|
+
"COMPLEX",
|
|
87
|
+
"REASONING"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"signal": "anchor",
|
|
92
|
+
"band_in": [
|
|
93
|
+
"COMPLEX",
|
|
94
|
+
"REASONING"
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"signal": "forensics"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"signal": "complexity",
|
|
102
|
+
"min": 0.6
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"tier": "COMPLEX",
|
|
109
|
+
"effort": "medium",
|
|
110
|
+
"hosts": [
|
|
111
|
+
"DeepSeek"
|
|
112
|
+
],
|
|
113
|
+
"plugins": {
|
|
114
|
+
"grounding": {
|
|
115
|
+
"mode": "observe",
|
|
116
|
+
"contradiction_threshold": 0.6
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "harness_easy_task",
|
|
122
|
+
"priority": 100,
|
|
123
|
+
"rules": {
|
|
124
|
+
"operator": "AND",
|
|
125
|
+
"conditions": [
|
|
126
|
+
{
|
|
127
|
+
"signal": "harness"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"tier": "MEDIUM",
|
|
132
|
+
"effort": "low",
|
|
133
|
+
"hosts": [
|
|
134
|
+
"Friendli",
|
|
135
|
+
"Parasail"
|
|
136
|
+
],
|
|
137
|
+
"plugins": {
|
|
138
|
+
"response_cache": false,
|
|
139
|
+
"format_guard": false,
|
|
140
|
+
"grounding": {
|
|
141
|
+
"mode": "observe",
|
|
142
|
+
"contradiction_threshold": 0.6
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "structured_output_no_mutation",
|
|
148
|
+
"priority": 50,
|
|
149
|
+
"rules": {
|
|
150
|
+
"operator": "AND",
|
|
151
|
+
"conditions": [
|
|
152
|
+
{
|
|
153
|
+
"signal": "structured"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
"tier": "from:legacy",
|
|
158
|
+
"plugins": {
|
|
159
|
+
"response_cache": false,
|
|
160
|
+
"format_guard": false
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "legacy",
|
|
165
|
+
"priority": 0,
|
|
166
|
+
"rules": {
|
|
167
|
+
"operator": "AND",
|
|
168
|
+
"conditions": []
|
|
169
|
+
},
|
|
170
|
+
"tier": "from:legacy"
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
"switch_gate": {
|
|
174
|
+
"mode": "observe",
|
|
175
|
+
"escalate_after_regressions": 2,
|
|
176
|
+
"downgrade_after_recoveries": 3,
|
|
177
|
+
"min_turns_before_switch": 2,
|
|
178
|
+
"max_switches_per_session": 2,
|
|
179
|
+
"cooldown_turns": 2
|
|
180
|
+
},
|
|
181
|
+
"cascade": {
|
|
182
|
+
"mode": "observe",
|
|
183
|
+
"trigger_margin": 0.15,
|
|
184
|
+
"escalate_to": "COMPLEX"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"mode": "observe",
|
|
4
|
+
"anchor_bands": {
|
|
5
|
+
"SIMPLE": [
|
|
6
|
+
0,
|
|
7
|
+
20
|
|
8
|
+
],
|
|
9
|
+
"MEDIUM": [
|
|
10
|
+
20,
|
|
11
|
+
51
|
|
12
|
+
],
|
|
13
|
+
"COMPLEX": [
|
|
14
|
+
51,
|
|
15
|
+
76
|
|
16
|
+
],
|
|
17
|
+
"REASONING": [
|
|
18
|
+
76,
|
|
19
|
+
101
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"harness": {
|
|
23
|
+
"patterns": [
|
|
24
|
+
{
|
|
25
|
+
"name": "terminus",
|
|
26
|
+
"preamble": "You are an AI assistant tasked with solving command-line tasks",
|
|
27
|
+
"instruction": "(?:^|\\n)Instruction:\\s*\\n([\\s\\S]*?)\\n\\s*\\n(?:Your response must be|Your response|Respond)"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"signals": {
|
|
32
|
+
"anchor": {
|
|
33
|
+
"type": "anchor_score"
|
|
34
|
+
},
|
|
35
|
+
"judge": {
|
|
36
|
+
"type": "jev"
|
|
37
|
+
},
|
|
38
|
+
"harness": {
|
|
39
|
+
"type": "harness"
|
|
40
|
+
},
|
|
41
|
+
"structured": {
|
|
42
|
+
"type": "request_field",
|
|
43
|
+
"any_of": [
|
|
44
|
+
"output_format",
|
|
45
|
+
"response_format"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
"tool_count": {
|
|
49
|
+
"type": "tool_count"
|
|
50
|
+
},
|
|
51
|
+
"turn": {
|
|
52
|
+
"type": "session_turn"
|
|
53
|
+
},
|
|
54
|
+
"phase": {
|
|
55
|
+
"type": "session_phase"
|
|
56
|
+
},
|
|
57
|
+
"risk": {
|
|
58
|
+
"type": "risk"
|
|
59
|
+
},
|
|
60
|
+
"prev": {
|
|
61
|
+
"type": "prev_outcome"
|
|
62
|
+
},
|
|
63
|
+
"complexity": {
|
|
64
|
+
"type": "complexity",
|
|
65
|
+
"threshold": 0.6,
|
|
66
|
+
"scale": 0.15
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"decisions": [
|
|
70
|
+
{
|
|
71
|
+
"name": "legacy",
|
|
72
|
+
"priority": 0,
|
|
73
|
+
"rules": {
|
|
74
|
+
"operator": "AND",
|
|
75
|
+
"conditions": []
|
|
76
|
+
},
|
|
77
|
+
"tier": "from:legacy"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"switch_gate": {
|
|
81
|
+
"mode": "observe",
|
|
82
|
+
"escalate_after_regressions": 2,
|
|
83
|
+
"downgrade_after_recoveries": 3,
|
|
84
|
+
"min_turns_before_switch": 2,
|
|
85
|
+
"max_switches_per_session": 2,
|
|
86
|
+
"cooldown_turns": 2
|
|
87
|
+
},
|
|
88
|
+
"cascade": {
|
|
89
|
+
"mode": "observe",
|
|
90
|
+
"trigger_margin": 0.15,
|
|
91
|
+
"escalate_to": "COMPLEX"
|
|
92
|
+
}
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.14.
|
|
3
|
+
"version": "9.14.18",
|
|
4
4
|
"files": [
|
|
5
5
|
"index.js",
|
|
6
6
|
"install.sh",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"start:supervised": "while true; do node index.js 2>&1 | npx pino-pretty --sync; code=$?; echo \"[supervisor] lynkr exited ($code) — restarting in 3s\"; sleep 3; done",
|
|
39
39
|
"lint": "eslint src index.js",
|
|
40
40
|
"test": "npm run test:unit && npm run test:performance",
|
|
41
|
-
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/harness-envelope.test.js test/task-ledger.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js",
|
|
41
|
+
"test:unit": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com LOG_FILE_ENABLED=false node --test test/routing.test.js test/hybrid-routing-integration.test.js test/retry-logic.test.js test/sse-transformer.test.js test/passthrough-stream.test.js test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js test/azure-openai-config.test.js test/azure-openai-format-conversion.test.js test/azure-openai-routing.test.js test/azure-openai-streaming.test.js test/azure-openai-error-resilience.test.js test/azure-openai-integration.test.js test/openai-integration.test.js test/atlas-integration.test.js test/toon-compression.test.js test/gcf-compression.test.js test/llamacpp-integration.test.js test/resilience.test.js test/telemetry-routing.test.js test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js test/distill.test.js test/large-payload.test.js test/prompt-cache-injection.test.js test/risk-analyzer.test.js test/interaction-block.test.js test/preflight.test.js test/token-reduction.test.js test/session-affinity.test.js test/cache-state.test.js test/cache-switch-cost.test.js test/lens-recommendations.test.js test/model-registry-cost.test.js test/output-format-guard.test.js test/tier-fallback.test.js test/wrap.test.js test/init.test.js test/tool-call-response-metadata.test.js test/degradation.test.js test/routing-telemetry-columns.test.js test/sticky-routing.test.js test/knn-ambiguous-escalate.test.js test/deescalator.test.js test/client-profiles.test.js test/strip-internal-fields.test.js test/complexity-tool-subtraction.test.js test/bandit.test.js test/routing-propensity.test.js test/reward-pipeline.test.js test/knn-cold-start.test.js test/calibration.test.js test/feedback-loop.test.js test/session-fingerprint.test.js test/side-request-guards.test.js test/verifier.test.js test/intent-score.test.js test/difficulty-classifier.test.js test/classifier-setup.test.js test/usage-stats.test.js test/loop-guard.test.js test/moonshot-model-mapping.test.js test/baidu-model-mapping.test.js test/tenant-policy-ingress-parity.test.js test/decide.test.js test/embeddings-degradation.test.js test/health-probe.test.js test/stuck-detector.test.js test/onnx-embedder.test.js test/ope.test.js test/hierarchical-budget.test.js test/token-rate-limit.test.js test/otel-export.test.js test/mcp-broker.test.js test/compression-budget.test.js test/gpt-utils.test.js test/dedup-observe-only.test.js test/context-window-header.test.js test/token-budget-auto.test.js test/opencode-setup.test.js test/auth-mode-first-party.test.js test/harness-envelope.test.js test/task-ledger.test.js test/jev-router.test.js test/jev-routing.test.js test/force-patterns.test.js test/upstream-fidelity.test.js test/tool-schema-compression.test.js test/passthrough-route.test.js test/quota-ledger.test.js test/shortfall-semantic.test.js test/routing-decisions.test.js test/routing-corpus.test.js test/switch-gate-grounding.test.js",
|
|
42
42
|
"test:memory": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/memory/store.test.js test/memory/surprise.test.js test/memory/extractor.test.js test/memory/search.test.js test/memory/retriever.test.js test/memory/distiller.test.js test/memory/distiller-freeze.test.js test/memory/wiki.test.js test/memory/skills-cache.test.js test/memory/tencentdb-launcher.test.js",
|
|
43
43
|
"test:new-features": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node --test test/passthrough-mode.test.js test/openrouter-error-resilience.test.js test/format-conversion.test.js",
|
|
44
44
|
"test:performance": "LYNKR_KNN_DIR=/tmp/lynkr-test-knn DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/hybrid-routing-performance.test.js && DATABRICKS_API_KEY=test-key DATABRICKS_API_BASE=http://test.com node test/performance-tests.js",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build config/complexity-exemplars.json from measured task outcomes.
|
|
4
|
+
*
|
|
5
|
+
* "Hard" exemplars are task instructions the STRONG model failed in every
|
|
6
|
+
* solo run supplied; "easy" exemplars are instructions the CHEAP model passed.
|
|
7
|
+
* Everything else is left out (ambiguous). The complexity signal
|
|
8
|
+
* (signals.js, type: complexity) embeds both sets once and scores a request
|
|
9
|
+
* as sim(hard) − sim(easy), normalised to [0,1].
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/build-complexity-exemplars.js \
|
|
13
|
+
* --strong ~/tb-runs-auto-v2 [--strong ~/tb-runs-auto-full] \
|
|
14
|
+
* --cheap ~/tb-runs-lynkr-v14 --cheap-model glm-5.3-flash \
|
|
15
|
+
* [--out config/complexity-exemplars.json]
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
function expand(p) { return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : path.resolve(p); }
|
|
24
|
+
function findRunRoot(dir) {
|
|
25
|
+
if (fs.existsSync(path.join(dir, 'results.json'))) return dir;
|
|
26
|
+
const subs = fs.readdirSync(dir).filter((d) => fs.existsSync(path.join(dir, d, 'results.json'))).sort().reverse();
|
|
27
|
+
return subs.length ? path.join(dir, subs[0]) : null;
|
|
28
|
+
}
|
|
29
|
+
function loadRun(root) {
|
|
30
|
+
const res = JSON.parse(fs.readFileSync(path.join(root, 'results.json'), 'utf8')).results || [];
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const r of res) out[r.task_id] = { resolved: !!r.is_resolved, instruction: r.instruction || null };
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function servedBy(root, task, needle) {
|
|
36
|
+
// share of episodes whose debug record names the model
|
|
37
|
+
const tdir = path.join(root, task); let trial;
|
|
38
|
+
try { trial = fs.readdirSync(tdir).find((x) => fs.existsSync(path.join(tdir, x, 'agent-logs'))); } catch { return 0; }
|
|
39
|
+
if (!trial) return 0;
|
|
40
|
+
const eps = fs.readdirSync(path.join(tdir, trial, 'agent-logs')).filter((e) => e.startsWith('episode-'));
|
|
41
|
+
let hit = 0, n = 0;
|
|
42
|
+
for (const e of eps) {
|
|
43
|
+
try { const s = JSON.parse(fs.readFileSync(path.join(tdir, trial, 'agent-logs', e, 'debug.json'), 'utf8')).original_response || ''; n++; if (s.toLowerCase().includes(needle.toLowerCase())) hit++; } catch { /* skip */ }
|
|
44
|
+
}
|
|
45
|
+
return n ? hit / n : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function main() {
|
|
49
|
+
const argv = process.argv.slice(2);
|
|
50
|
+
const strong = [], cheap = []; let cheapModel = null, out = path.join(__dirname, '..', 'config', 'complexity-exemplars.json');
|
|
51
|
+
for (let i = 0; i < argv.length; i++) {
|
|
52
|
+
if (argv[i] === '--strong') strong.push(expand(argv[++i]));
|
|
53
|
+
else if (argv[i] === '--cheap') cheap.push(expand(argv[++i]));
|
|
54
|
+
else if (argv[i] === '--cheap-model') cheapModel = argv[++i];
|
|
55
|
+
else if (argv[i] === '--out') out = expand(argv[++i]);
|
|
56
|
+
}
|
|
57
|
+
if (!strong.length) { console.error('need at least one --strong run dir'); process.exit(2); }
|
|
58
|
+
const strongRuns = strong.map(findRunRoot).filter(Boolean).map(loadRun);
|
|
59
|
+
const cheapRoots = cheap.map(findRunRoot).filter(Boolean);
|
|
60
|
+
const cheapRuns = cheapRoots.map(loadRun);
|
|
61
|
+
const tasks = new Set(); strongRuns.forEach((r) => Object.keys(r).forEach((t) => tasks.add(t)));
|
|
62
|
+
const hard = [], easy = [], skipped = [];
|
|
63
|
+
for (const t of tasks) {
|
|
64
|
+
const instr = strongRuns.map((r) => r[t]?.instruction).find(Boolean); if (!instr) continue;
|
|
65
|
+
const strongFailedAll = strongRuns.every((r) => r[t] && !r[t].resolved);
|
|
66
|
+
const strongPassedAny = strongRuns.some((r) => r[t] && r[t].resolved);
|
|
67
|
+
let cheapPassed = false;
|
|
68
|
+
cheapRuns.forEach((r, i) => { if (r[t]?.resolved && (!cheapModel || servedBy(cheapRoots[i], t, cheapModel) >= 0.8)) cheapPassed = true; });
|
|
69
|
+
if (strongFailedAll) hard.push({ task: t, text: instr.slice(0, 1200) });
|
|
70
|
+
else if (cheapPassed) easy.push({ task: t, text: instr.slice(0, 1200) });
|
|
71
|
+
else if (strongPassedAny) skipped.push(t);
|
|
72
|
+
}
|
|
73
|
+
const heads = ['reasoning', 'codegen', 'debugging', 'tool_use'];
|
|
74
|
+
const doc = {
|
|
75
|
+
generated: new Date().toISOString(),
|
|
76
|
+
note: 'hard = strong model failed every supplied solo run; easy = cheap model passed. Edit freely; the signal embeds these at load.',
|
|
77
|
+
// One shared pool per side; per-head pools can be curated later.
|
|
78
|
+
heads: Object.fromEntries(heads.map((h) => [h, { hard: hard.map((x) => x.text), easy: easy.map((x) => x.text) }])),
|
|
79
|
+
provenance: { hard: hard.map((x) => x.task), easy: easy.map((x) => x.task), ambiguous: skipped },
|
|
80
|
+
};
|
|
81
|
+
fs.writeFileSync(out, JSON.stringify(doc, null, 1));
|
|
82
|
+
console.log(`hard ${hard.length}, easy ${easy.length}, ambiguous ${skipped.length} → ${out}`);
|
|
83
|
+
}
|
|
84
|
+
main();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build / refresh the routing corpus fixtures used by test/routing-corpus.test.js.
|
|
4
|
+
*
|
|
5
|
+
* For every recorded request (a directory tree of Terminal-Bench runs, or any
|
|
6
|
+
* directory of *.json request bodies), run the full routing decision under
|
|
7
|
+
* the given config and store the signal snapshot plus the decision reached.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/build-routing-corpus.js --from <run-dir|requests-dir> \
|
|
11
|
+
* --config config/routing.example.json --out test/fixtures/routing-corpus/<name>.json
|
|
12
|
+
*
|
|
13
|
+
* Needs the operator .env (embeddings, judge key) like the server does.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
function arg(name, dflt) { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : dflt; }
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const from = arg('--from'); const cfgPath = arg('--config', 'config/routing.example.json'); const out = arg('--out');
|
|
25
|
+
if (!from || !out) { console.error('usage: --from <dir> --config <routing.json> --out <fixture.json>'); process.exit(2); }
|
|
26
|
+
const envPath = fs.existsSync(path.join(process.cwd(), '.env')) ? path.join(process.cwd(), '.env') : path.join(os.homedir(), '.env');
|
|
27
|
+
require('dotenv').config({ path: envPath });
|
|
28
|
+
process.env.LOG_LEVEL = 'silent'; process.env.LOG_FILE_ENABLED = 'false';
|
|
29
|
+
process.env.LYNKR_ROUTING_CONFIG = path.resolve(cfgPath);
|
|
30
|
+
const { determineProviderSmart } = require('../src/routing');
|
|
31
|
+
|
|
32
|
+
// Collect requests: Terminal-Bench layout (<task>/<trial>/agent-logs/episode-0/prompt.txt) or *.json bodies.
|
|
33
|
+
const requests = [];
|
|
34
|
+
for (const entry of fs.readdirSync(from)) {
|
|
35
|
+
const p = path.join(from, entry);
|
|
36
|
+
if (entry.endsWith('.json') && fs.statSync(p).isFile()) { requests.push({ name: entry.replace(/\.json$/, ''), payload: JSON.parse(fs.readFileSync(p, 'utf8')) }); continue; }
|
|
37
|
+
if (!fs.statSync(p).isDirectory()) continue;
|
|
38
|
+
const trial = fs.readdirSync(p).find((x) => fs.existsSync(path.join(p, x, 'agent-logs', 'episode-0', 'prompt.txt')));
|
|
39
|
+
if (trial) requests.push({ name: entry, payload: { messages: [{ role: 'user', content: fs.readFileSync(path.join(p, trial, 'agent-logs', 'episode-0', 'prompt.txt'), 'utf8') }], tools: [] } });
|
|
40
|
+
}
|
|
41
|
+
const fixtures = []; const dist = {};
|
|
42
|
+
for (const r of requests) {
|
|
43
|
+
const d = await determineProviderSmart(r.payload, {});
|
|
44
|
+
const e = d.engine; if (!e) continue;
|
|
45
|
+
const signals = {};
|
|
46
|
+
for (const [k, v] of Object.entries(e.signals)) signals[k] = { matched: v.matched, value: (v.value && typeof v.value === 'object') ? null : v.value, confidence: v.confidence, band: v.band, tier: v.tier };
|
|
47
|
+
fixtures.push({ task: r.name, legacyTier: d.tier, signals, expected: { decision: e.decision, tier: e.tier, effort: e.effort } });
|
|
48
|
+
dist[e.decision] = (dist[e.decision] || 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
51
|
+
fs.writeFileSync(out, JSON.stringify({ generated: new Date().toISOString(), config: path.relative(process.cwd(), path.resolve(cfgPath)), fixtures }, null, 1));
|
|
52
|
+
console.log(`wrote ${fixtures.length} fixtures → ${out}; decisions: ${JSON.stringify(dist)}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prompt-cache probe: does each configured OpenRouter host actually serve
|
|
4
|
+
* cache hits, and what does warm vs cold cost in time?
|
|
5
|
+
*
|
|
6
|
+
* Sends the same ~3k-token prefix twice to every (model, host) pair from
|
|
7
|
+
* OPENROUTER_PROVIDER_ORDER_MAP (or --model/--host), reports cached_tokens,
|
|
8
|
+
* cache price, TTFT-ish latency for both calls, and the provider-billed cost.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node scripts/cache-probe.js [--model <id> --host <name>]... [--prefix-tokens 3000]
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const envPath = fs.existsSync(path.join(process.cwd(), '.env')) ? path.join(process.cwd(), '.env') : path.join(os.homedir(), '.env');
|
|
20
|
+
require('dotenv').config({ path: envPath });
|
|
21
|
+
const KEY = process.env.OPENROUTER_API_KEY;
|
|
22
|
+
if (!KEY) { console.error('OPENROUTER_API_KEY not set'); process.exit(2); }
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
const pairs = [];
|
|
25
|
+
for (let i = 0; i < argv.length; i++) if (argv[i] === '--model') pairs.push({ model: argv[++i], host: argv[i + 1] === '--host' ? argv[(i += 2)] : null });
|
|
26
|
+
if (!pairs.length) {
|
|
27
|
+
const map = (process.env.OPENROUTER_PROVIDER_ORDER_MAP || '').replace(/^"|"$/g, '');
|
|
28
|
+
for (const entry of map.split(';')) {
|
|
29
|
+
const eq = entry.indexOf('='); if (eq < 0) continue;
|
|
30
|
+
const k = entry.slice(0, eq).trim(); const hosts = entry.slice(eq + 1).split(',').map((x) => x.trim()).filter(Boolean);
|
|
31
|
+
// Resolve the short model key to a configured TIER_* model id.
|
|
32
|
+
const tiers = ['TIER_SIMPLE', 'TIER_MEDIUM', 'TIER_COMPLEX', 'TIER_REASONING'].map((t) => process.env[t] || '').filter((v) => v.startsWith('openrouter:') && v.includes(k));
|
|
33
|
+
const model = tiers.length ? tiers[0].slice('openrouter:'.length).split(/\s+#/)[0].trim() : k;
|
|
34
|
+
for (const h of hosts) pairs.push({ model, host: h });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pi = argv.indexOf('--prefix-tokens'); const prefixTokens = pi >= 0 ? Number(argv[pi + 1]) || 3000 : 3000;
|
|
38
|
+
const filler = Array.from({ length: Math.round(prefixTokens / 12) }, (_, i) => `-rw-r--r-- 1 root root ${100000 + i * 37} Oct 2 05:00 file_${i}.log`).join('\n');
|
|
39
|
+
const nonce = Date.now();
|
|
40
|
+
const prefix = `[cache-probe ${nonce}]\nYou are an AI assistant tasked with solving command-line tasks in a Linux environment.\n\nInstruction:\nSummarise the directory listing below in one sentence.\n\n${filler}\n\n`;
|
|
41
|
+
|
|
42
|
+
const call = async (model, host, suffix) => {
|
|
43
|
+
const body = { model, messages: [{ role: 'user', content: prefix + suffix }], max_tokens: 64, temperature: 0, usage: { include: true } };
|
|
44
|
+
if (host) body.provider = { order: [host], allow_fallbacks: false };
|
|
45
|
+
const t0 = Date.now();
|
|
46
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', { method: 'POST', headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
47
|
+
const ms = Date.now() - t0; const j = await res.json();
|
|
48
|
+
if (j.error) return { error: j.error.message?.slice(0, 80), ms };
|
|
49
|
+
const u = j.usage || {};
|
|
50
|
+
return { ms, provider: j.provider, prompt: u.prompt_tokens, cached: u.prompt_tokens_details?.cached_tokens ?? 0, cost: u.cost ?? null };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
console.log(`prefix ≈ ${prefixTokens} tokens, two calls per host\n`);
|
|
54
|
+
console.log(`${'model'.padEnd(34)} ${'host'.padEnd(12)} ${'cold ms'.padStart(8)} ${'warm ms'.padStart(8)} ${'cached'.padStart(7)} ${'hit%'.padStart(5)} ${'cold $'.padStart(9)} ${'warm $'.padStart(9)} note`);
|
|
55
|
+
for (const { model, host } of pairs) {
|
|
56
|
+
const a = await call(model, host, 'Reply with one word: ready.');
|
|
57
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
58
|
+
const b = await call(model, host, 'Reply with one word: again.');
|
|
59
|
+
if (a.error || b.error) { console.log(`${model.padEnd(34)} ${String(host).padEnd(12)} ERROR ${a.error || b.error}`); continue; }
|
|
60
|
+
const hit = b.prompt ? Math.round(100 * b.cached / b.prompt) : 0;
|
|
61
|
+
const note = b.cached === 0 ? 'NO CACHE HITS REPORTED' : (b.ms < a.ms * 0.8 ? 'warm faster' : 'no latency gain');
|
|
62
|
+
console.log(`${model.padEnd(34)} ${String(host).padEnd(12)} ${String(a.ms).padStart(8)} ${String(b.ms).padStart(8)} ${String(b.cached).padStart(7)} ${String(hit).padStart(5)} ${a.cost != null ? a.cost.toFixed(6).padStart(9) : ' -'} ${b.cost != null ? b.cost.toFixed(6).padStart(9) : ' -'} ${note}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|