@antseed/router-core 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,22 +1,25 @@
1
1
  {
2
2
  "name": "@antseed/router-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Shared router infrastructure for Antseed plugins",
5
5
  "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
6
9
  "main": "dist/index.js",
7
10
  "types": "dist/index.d.ts",
8
- "scripts": {
9
- "prebuild": "rm -rf dist",
10
- "build": "tsc",
11
- "test": "vitest run",
12
- "typecheck": "tsc --noEmit"
13
- },
14
11
  "peerDependencies": {
15
12
  "@antseed/node": ">=0.1.0"
16
13
  },
17
14
  "devDependencies": {
18
15
  "typescript": "^5.5.0",
19
16
  "vitest": "^2.0.0",
20
- "@antseed/node": "workspace:*"
17
+ "@antseed/node": "0.1.1"
18
+ },
19
+ "scripts": {
20
+ "prebuild": "rm -rf dist",
21
+ "build": "tsc",
22
+ "test": "vitest run",
23
+ "typecheck": "tsc --noEmit"
21
24
  }
22
- }
25
+ }
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export {
2
- scoreCandidates,
3
- DEFAULT_WEIGHTS,
4
- type ScoringWeights,
5
- type ScoredCandidate,
6
- type TokenPricingUsdPerMillion,
7
- type PeerMetrics,
8
- } from './peer-scorer.js'
9
- export {
10
- PeerMetricsTracker,
11
- type PeerMetricsTrackerConfig,
12
- } from './peer-metrics.js'
13
- export {
14
- WELL_KNOWN_TOOL_HINTS,
15
- formatToolHints,
16
- type ToolHint,
17
- } from './tool-hints.js'
@@ -1,192 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { PeerMetricsTracker } from './peer-metrics.js'
3
-
4
- describe('PeerMetricsTracker', () => {
5
- describe('latency EMA calculation', () => {
6
- it('initializes EMA to the first latency value', () => {
7
- const tracker = new PeerMetricsTracker()
8
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
9
-
10
- const metrics = tracker.getMetrics('peer1')
11
- // First call: prev = 100 (default to result), EMA = 100 * 0.7 + 100 * 0.3 = 100
12
- expect(metrics.latencyEma).toBe(100)
13
- })
14
-
15
- it('applies exponential moving average on subsequent results', () => {
16
- const tracker = new PeerMetricsTracker({ latencyAlpha: 0.3 })
17
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
18
- tracker.recordResult('peer1', { success: true, latencyMs: 200 })
19
-
20
- const metrics = tracker.getMetrics('peer1')
21
- // After first: EMA = 100
22
- // After second: EMA = 100 * 0.7 + 200 * 0.3 = 70 + 60 = 130
23
- expect(metrics.latencyEma).toBe(130)
24
- })
25
-
26
- it('respects custom latencyAlpha', () => {
27
- const tracker = new PeerMetricsTracker({ latencyAlpha: 0.5 })
28
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
29
- tracker.recordResult('peer1', { success: true, latencyMs: 200 })
30
-
31
- const metrics = tracker.getMetrics('peer1')
32
- // After first: EMA = 100
33
- // After second: EMA = 100 * 0.5 + 200 * 0.5 = 150
34
- expect(metrics.latencyEma).toBe(150)
35
- })
36
- })
37
-
38
- describe('failure streak tracking', () => {
39
- it('increments failure streak on consecutive failures', () => {
40
- const tracker = new PeerMetricsTracker()
41
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
42
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
43
-
44
- const metrics = tracker.getMetrics('peer1')
45
- expect(metrics.failureStreak).toBe(2)
46
- expect(metrics.totalFailures).toBe(2)
47
- expect(metrics.totalAttempts).toBe(2)
48
- })
49
-
50
- it('success resets failure streak', () => {
51
- const tracker = new PeerMetricsTracker()
52
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
53
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
54
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
55
-
56
- const metrics = tracker.getMetrics('peer1')
57
- expect(metrics.failureStreak).toBe(0)
58
- // Total failures and attempts remain
59
- expect(metrics.totalFailures).toBe(2)
60
- expect(metrics.totalAttempts).toBe(3)
61
- })
62
-
63
- it('tracks total failures independently from streak', () => {
64
- const tracker = new PeerMetricsTracker()
65
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
66
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
67
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
68
-
69
- const metrics = tracker.getMetrics('peer1')
70
- expect(metrics.failureStreak).toBe(1)
71
- expect(metrics.totalFailures).toBe(2)
72
- expect(metrics.totalAttempts).toBe(3)
73
- })
74
- })
75
-
76
- describe('cooldown timing', () => {
77
- it('enters cooldown after maxFailures consecutive failures', () => {
78
- let now = 1_000_000
79
- const tracker = new PeerMetricsTracker({
80
- maxFailures: 2,
81
- failureCooldownMs: 500,
82
- now: () => now,
83
- })
84
-
85
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
86
- expect(tracker.isCoolingDown('peer1')).toBe(false)
87
-
88
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
89
- expect(tracker.isCoolingDown('peer1')).toBe(true)
90
- })
91
-
92
- it('cooldown expires after failureCooldownMs', () => {
93
- let now = 1_000_000
94
- const tracker = new PeerMetricsTracker({
95
- maxFailures: 2,
96
- failureCooldownMs: 500,
97
- now: () => now,
98
- })
99
-
100
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
101
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
102
- expect(tracker.isCoolingDown('peer1')).toBe(true)
103
-
104
- now += 501
105
- expect(tracker.isCoolingDown('peer1')).toBe(false)
106
- })
107
-
108
- it('success clears cooldown', () => {
109
- let now = 1_000_000
110
- const tracker = new PeerMetricsTracker({
111
- maxFailures: 2,
112
- failureCooldownMs: 500,
113
- now: () => now,
114
- })
115
-
116
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
117
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
118
- expect(tracker.isCoolingDown('peer1')).toBe(true)
119
-
120
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
121
- expect(tracker.isCoolingDown('peer1')).toBe(false)
122
- })
123
-
124
- it('applies exponential backoff for repeated failure streaks beyond threshold', () => {
125
- let now = 1_000_000
126
- const tracker = new PeerMetricsTracker({
127
- maxFailures: 2,
128
- failureCooldownMs: 500,
129
- now: () => now,
130
- })
131
-
132
- // 2 failures: cooldown = 500 * 2^0 = 500
133
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
134
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
135
-
136
- const metrics2 = tracker.getMetrics('peer1')
137
- expect(metrics2.cooldownUntil).toBe(now + 500)
138
-
139
- // 3rd failure: cooldown = 500 * 2^1 = 1000
140
- now += 501 // past first cooldown
141
- tracker.recordResult('peer1', { success: false, latencyMs: 100 })
142
-
143
- const metrics3 = tracker.getMetrics('peer1')
144
- expect(metrics3.cooldownUntil).toBe(now + 1000)
145
- })
146
- })
147
-
148
- describe('median latency', () => {
149
- it('returns 500 when no latencies recorded', () => {
150
- const tracker = new PeerMetricsTracker()
151
- expect(tracker.getMedianLatency()).toBe(500)
152
- })
153
-
154
- it('returns single value when only one peer has latency', () => {
155
- const tracker = new PeerMetricsTracker()
156
- tracker.recordResult('peer1', { success: true, latencyMs: 200 })
157
- expect(tracker.getMedianLatency()).toBe(200)
158
- })
159
-
160
- it('returns median of odd number of values', () => {
161
- const tracker = new PeerMetricsTracker()
162
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
163
- tracker.recordResult('peer2', { success: true, latencyMs: 300 })
164
- tracker.recordResult('peer3', { success: true, latencyMs: 200 })
165
-
166
- expect(tracker.getMedianLatency()).toBe(200)
167
- })
168
-
169
- it('returns average of two middle values for even count', () => {
170
- const tracker = new PeerMetricsTracker()
171
- tracker.recordResult('peer1', { success: true, latencyMs: 100 })
172
- tracker.recordResult('peer2', { success: true, latencyMs: 200 })
173
- tracker.recordResult('peer3', { success: true, latencyMs: 300 })
174
- tracker.recordResult('peer4', { success: true, latencyMs: 400 })
175
-
176
- expect(tracker.getMedianLatency()).toBe(250)
177
- })
178
- })
179
-
180
- describe('getMetrics', () => {
181
- it('returns zeroed metrics for unknown peer', () => {
182
- const tracker = new PeerMetricsTracker()
183
- const metrics = tracker.getMetrics('unknown')
184
-
185
- expect(metrics.latencyEma).toBeUndefined()
186
- expect(metrics.failureStreak).toBe(0)
187
- expect(metrics.totalFailures).toBe(0)
188
- expect(metrics.totalAttempts).toBe(0)
189
- expect(metrics.cooldownUntil).toBeUndefined()
190
- })
191
- })
192
- })
@@ -1,88 +0,0 @@
1
- import type { PeerMetrics } from './peer-scorer.js'
2
-
3
- export interface PeerMetricsTrackerConfig {
4
- latencyAlpha?: number // Default: 0.3
5
- maxFailures?: number // Default: 3
6
- failureCooldownMs?: number // Default: 30000
7
- now?: () => number
8
- }
9
-
10
- export class PeerMetricsTracker {
11
- private readonly _latencyMap = new Map<string, number>()
12
- private readonly _failureStreakMap = new Map<string, number>()
13
- private readonly _totalFailureMap = new Map<string, number>()
14
- private readonly _attemptMap = new Map<string, number>()
15
- private readonly _cooldownUntilMap = new Map<string, number>()
16
-
17
- private readonly _latencyAlpha: number
18
- private readonly _maxFailures: number
19
- private readonly _failureCooldownMs: number
20
- private readonly _now: () => number
21
-
22
- constructor(config?: PeerMetricsTrackerConfig) {
23
- this._latencyAlpha = config?.latencyAlpha ?? 0.3
24
- this._maxFailures = Math.max(1, config?.maxFailures ?? 3)
25
- this._failureCooldownMs = Math.max(1, config?.failureCooldownMs ?? 30_000)
26
- this._now = config?.now ?? (() => Date.now())
27
- }
28
-
29
- recordResult(peerId: string, result: { success: boolean; latencyMs: number }): void {
30
- const now = this._now()
31
- const attempts = (this._attemptMap.get(peerId) ?? 0) + 1
32
- this._attemptMap.set(peerId, attempts)
33
-
34
- if (result.success) {
35
- // Update latency EMA
36
- const prev = this._latencyMap.get(peerId) ?? result.latencyMs
37
- this._latencyMap.set(
38
- peerId,
39
- prev * (1 - this._latencyAlpha) + result.latencyMs * this._latencyAlpha,
40
- )
41
- // Reset failure count on success
42
- this._failureStreakMap.delete(peerId)
43
- this._cooldownUntilMap.delete(peerId)
44
- } else {
45
- const currentStreak = this._failureStreakMap.get(peerId) ?? 0
46
- const nextStreak = currentStreak + 1
47
- this._failureStreakMap.set(peerId, nextStreak)
48
- this._totalFailureMap.set(peerId, (this._totalFailureMap.get(peerId) ?? 0) + 1)
49
-
50
- if (nextStreak >= this._maxFailures) {
51
- const penaltyPower = Math.min(4, nextStreak - this._maxFailures)
52
- const cooldownMs = this._failureCooldownMs * (2 ** penaltyPower)
53
- this._cooldownUntilMap.set(peerId, now + cooldownMs)
54
- }
55
- }
56
- }
57
-
58
- getMetrics(peerId: string): PeerMetrics {
59
- return {
60
- latencyEma: this._latencyMap.get(peerId),
61
- failureStreak: this._failureStreakMap.get(peerId) ?? 0,
62
- totalFailures: this._totalFailureMap.get(peerId) ?? 0,
63
- totalAttempts: this._attemptMap.get(peerId) ?? 0,
64
- cooldownUntil: this._cooldownUntilMap.get(peerId),
65
- }
66
- }
67
-
68
- isCoolingDown(peerId: string): boolean {
69
- const now = this._now()
70
- const until = this._cooldownUntilMap.get(peerId)
71
- if (until === undefined) return false
72
- if (until <= now) {
73
- this._cooldownUntilMap.delete(peerId)
74
- return false
75
- }
76
- return true
77
- }
78
-
79
- getMedianLatency(): number {
80
- const values = [...this._latencyMap.values()]
81
- if (values.length === 0) return 500
82
- values.sort((a, b) => a - b)
83
- const mid = Math.floor(values.length / 2)
84
- return values.length % 2 === 0
85
- ? (values[mid - 1]! + values[mid]!) / 2
86
- : values[mid]!
87
- }
88
- }
@@ -1,195 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import type { PeerInfo } from '@antseed/node'
3
- import { scoreCandidates, DEFAULT_WEIGHTS } from './peer-scorer.js'
4
- import type { PeerMetrics, TokenPricingUsdPerMillion } from './peer-scorer.js'
5
-
6
- function makePeerId(char: string): PeerInfo['peerId'] {
7
- return char.repeat(64) as PeerInfo['peerId']
8
- }
9
-
10
- function makePeer(overrides?: Partial<PeerInfo>): PeerInfo {
11
- return {
12
- peerId: makePeerId('a'),
13
- lastSeen: 1_000_000,
14
- providers: ['anthropic'],
15
- reputationScore: 80,
16
- trustScore: 80,
17
- maxConcurrency: 10,
18
- currentLoad: 1,
19
- ...overrides,
20
- }
21
- }
22
-
23
- const defaultMetrics: PeerMetrics = {
24
- latencyEma: undefined,
25
- failureStreak: 0,
26
- totalFailures: 0,
27
- totalAttempts: 0,
28
- cooldownUntil: undefined,
29
- }
30
-
31
- const defaultOffer: TokenPricingUsdPerMillion = {
32
- inputUsdPerMillion: 10,
33
- outputUsdPerMillion: 10,
34
- }
35
-
36
- const defaultContext = {
37
- now: 1_000_000,
38
- medianLatency: 500,
39
- maxPeerStalenessMs: 300_000,
40
- maxFailures: 3,
41
- }
42
-
43
- describe('scoreCandidates', () => {
44
- it('returns correct score ordering — cheaper peer ranks higher', () => {
45
- const cheapPeer = makePeer({ peerId: makePeerId('1') })
46
- const expensivePeer = makePeer({ peerId: makePeerId('2') })
47
-
48
- const result = scoreCandidates(
49
- [
50
- {
51
- peer: expensivePeer,
52
- provider: 'anthropic',
53
- providerRank: 0,
54
- offer: { inputUsdPerMillion: 100, outputUsdPerMillion: 100 },
55
- metrics: defaultMetrics,
56
- },
57
- {
58
- peer: cheapPeer,
59
- provider: 'anthropic',
60
- providerRank: 0,
61
- offer: { inputUsdPerMillion: 5, outputUsdPerMillion: 5 },
62
- metrics: defaultMetrics,
63
- },
64
- ],
65
- defaultContext,
66
- )
67
-
68
- expect(result).toHaveLength(2)
69
- expect(result[0]!.peer.peerId).toBe(cheapPeer.peerId)
70
- expect(result[1]!.peer.peerId).toBe(expensivePeer.peerId)
71
- expect(result[0]!.score).toBeGreaterThan(result[1]!.score)
72
- })
73
-
74
- it('custom weights change ranking — zero out price, maximize latency', () => {
75
- const cheapButSlow = makePeer({ peerId: makePeerId('1') })
76
- const expensiveButFast = makePeer({ peerId: makePeerId('2') })
77
-
78
- const result = scoreCandidates(
79
- [
80
- {
81
- peer: cheapButSlow,
82
- provider: 'anthropic',
83
- providerRank: 0,
84
- offer: { inputUsdPerMillion: 1, outputUsdPerMillion: 1 },
85
- metrics: { ...defaultMetrics, latencyEma: 1000 },
86
- },
87
- {
88
- peer: expensiveButFast,
89
- provider: 'anthropic',
90
- providerRank: 0,
91
- offer: { inputUsdPerMillion: 100, outputUsdPerMillion: 100 },
92
- metrics: { ...defaultMetrics, latencyEma: 50 },
93
- },
94
- ],
95
- {
96
- ...defaultContext,
97
- weights: {
98
- price: 0,
99
- latency: 1.0,
100
- capacity: 0,
101
- reputation: 0,
102
- freshness: 0,
103
- reliability: 0,
104
- },
105
- },
106
- )
107
-
108
- expect(result[0]!.peer.peerId).toBe(expensiveButFast.peerId)
109
- })
110
-
111
- it('handles single candidate', () => {
112
- const peer = makePeer({ peerId: makePeerId('1') })
113
-
114
- const result = scoreCandidates(
115
- [
116
- {
117
- peer,
118
- provider: 'anthropic',
119
- providerRank: 0,
120
- offer: defaultOffer,
121
- metrics: defaultMetrics,
122
- },
123
- ],
124
- defaultContext,
125
- )
126
-
127
- expect(result).toHaveLength(1)
128
- expect(result[0]!.peer.peerId).toBe(peer.peerId)
129
- expect(result[0]!.score).toBeGreaterThan(0)
130
- })
131
-
132
- it('handles all equal scores — tie-broken by peerId', () => {
133
- const peerA = makePeer({ peerId: makePeerId('a') })
134
- const peerB = makePeer({ peerId: makePeerId('b') })
135
-
136
- const result = scoreCandidates(
137
- [
138
- {
139
- peer: peerB,
140
- provider: 'anthropic',
141
- providerRank: 0,
142
- offer: defaultOffer,
143
- metrics: defaultMetrics,
144
- },
145
- {
146
- peer: peerA,
147
- provider: 'anthropic',
148
- providerRank: 0,
149
- offer: defaultOffer,
150
- metrics: defaultMetrics,
151
- },
152
- ],
153
- defaultContext,
154
- )
155
-
156
- expect(result).toHaveLength(2)
157
- // Both have identical inputs so scores should be equal; tie-break by peerId ascending
158
- expect(result[0]!.peer.peerId).toBe(peerA.peerId)
159
- expect(result[1]!.peer.peerId).toBe(peerB.peerId)
160
- })
161
-
162
- it('returns empty array for empty candidates', () => {
163
- const result = scoreCandidates([], defaultContext)
164
- expect(result).toHaveLength(0)
165
- })
166
-
167
- it('uses default weights when none provided', () => {
168
- const peer = makePeer({ peerId: makePeerId('1') })
169
-
170
- const result = scoreCandidates(
171
- [
172
- {
173
- peer,
174
- provider: 'anthropic',
175
- providerRank: 0,
176
- offer: defaultOffer,
177
- metrics: defaultMetrics,
178
- },
179
- ],
180
- defaultContext,
181
- )
182
-
183
- // With a single candidate, all normalized-inverted factors are 1.0
184
- // Score = sum of all weights = 1.0 (minus any freshness/reputation adjustments)
185
- expect(result[0]!.score).toBeGreaterThan(0)
186
- expect(result[0]!.score).toBeLessThanOrEqual(
187
- DEFAULT_WEIGHTS.price +
188
- DEFAULT_WEIGHTS.latency +
189
- DEFAULT_WEIGHTS.capacity +
190
- DEFAULT_WEIGHTS.reputation +
191
- DEFAULT_WEIGHTS.freshness +
192
- DEFAULT_WEIGHTS.reliability,
193
- )
194
- })
195
- })
@@ -1,196 +0,0 @@
1
- import type { PeerInfo } from '@antseed/node'
2
-
3
- export interface TokenPricingUsdPerMillion {
4
- inputUsdPerMillion: number
5
- outputUsdPerMillion: number
6
- }
7
-
8
- export interface ScoringWeights {
9
- price: number
10
- latency: number
11
- capacity: number
12
- reputation: number
13
- freshness: number
14
- reliability: number
15
- }
16
-
17
- export const DEFAULT_WEIGHTS: ScoringWeights = {
18
- price: 0.30,
19
- latency: 0.25,
20
- capacity: 0.20,
21
- reputation: 0.10,
22
- freshness: 0.10,
23
- reliability: 0.05,
24
- }
25
-
26
- export interface PeerMetrics {
27
- latencyEma: number | undefined
28
- failureStreak: number
29
- totalFailures: number
30
- totalAttempts: number
31
- cooldownUntil: number | undefined
32
- }
33
-
34
- export interface ScoredCandidate {
35
- peer: PeerInfo
36
- provider: string
37
- providerRank: number
38
- offer: TokenPricingUsdPerMillion
39
- score: number
40
- }
41
-
42
- interface CandidateInput {
43
- peer: PeerInfo
44
- provider: string
45
- providerRank: number
46
- offer: TokenPricingUsdPerMillion
47
- metrics: PeerMetrics
48
- }
49
-
50
- interface ScoringContext {
51
- now: number
52
- medianLatency: number
53
- maxPeerStalenessMs: number
54
- maxFailures: number
55
- weights?: Partial<ScoringWeights>
56
- }
57
-
58
- function normalizedInverted(value: number, min: number, range: number): number {
59
- if (range <= 0) {
60
- return 1
61
- }
62
- return 1 - (value - min) / range
63
- }
64
-
65
- function effectiveReputation(p: PeerInfo): number {
66
- if (p.onChainReputation !== undefined) {
67
- return p.onChainReputation
68
- }
69
- return p.trustScore ?? p.reputationScore ?? 0
70
- }
71
-
72
- function availableCapacity(p: PeerInfo): number {
73
- const max = p.maxConcurrency ?? 1
74
- const current = p.currentLoad ?? 0
75
- return Math.max(0, max - current)
76
- }
77
-
78
- function freshnessFactor(p: PeerInfo, now: number, maxPeerStalenessMs: number): number {
79
- if (!Number.isFinite(p.lastSeen)) return 0.5
80
- const age = Math.max(0, now - p.lastSeen)
81
- if (age >= maxPeerStalenessMs) return 0
82
- return 1 - (age / maxPeerStalenessMs)
83
- }
84
-
85
- function reliabilityFactor(metrics: PeerMetrics, maxFailures: number): number {
86
- const historicalPenalty = metrics.totalAttempts > 0 ? metrics.totalFailures / metrics.totalAttempts : 0
87
- const streakPenalty = Math.min(1, metrics.failureStreak / maxFailures)
88
- return Math.max(0, 1 - Math.max(historicalPenalty, streakPenalty))
89
- }
90
-
91
- function peerLatency(metrics: PeerMetrics, medianLatency: number): number {
92
- return metrics.latencyEma ?? medianLatency
93
- }
94
-
95
- function tieBreak(a: PeerInfo, aLatencyEma: number | undefined, b: PeerInfo, bLatencyEma: number | undefined): number {
96
- const latA = aLatencyEma ?? Number.POSITIVE_INFINITY
97
- const latB = bLatencyEma ?? Number.POSITIVE_INFINITY
98
- if (latA !== latB) {
99
- return latA - latB
100
- }
101
- return a.peerId.localeCompare(b.peerId)
102
- }
103
-
104
- /**
105
- * Score and rank candidates using the composite scoring algorithm.
106
- *
107
- * Calculates normalization context (min/max price, latency, capacity),
108
- * scores each candidate with weighted factors, and returns candidates
109
- * sorted by score (highest first).
110
- */
111
- export function scoreCandidates(
112
- candidates: CandidateInput[],
113
- context: ScoringContext,
114
- ): ScoredCandidate[] {
115
- if (candidates.length === 0) return []
116
-
117
- const w: ScoringWeights = { ...DEFAULT_WEIGHTS, ...context.weights }
118
-
119
- // --- Normalization context ---
120
- const knownPrices = candidates
121
- .map((c) => c.offer.inputUsdPerMillion)
122
- .filter((value) => Number.isFinite(value))
123
- const fallbackPrice = knownPrices.length > 0 ? Math.max(...knownPrices) * 1.25 : 1
124
- let minPrice = knownPrices.length > 0 ? Math.min(...knownPrices) : fallbackPrice
125
- let maxPrice = knownPrices.length > 0 ? Math.max(...knownPrices) : fallbackPrice
126
- let minLatency = Number.POSITIVE_INFINITY
127
- let maxLatency = 0
128
- let maxCap = 0
129
-
130
- for (const c of candidates) {
131
- const price = c.offer.inputUsdPerMillion
132
- if (price < minPrice) minPrice = price
133
- if (price > maxPrice) maxPrice = price
134
-
135
- const lat = peerLatency(c.metrics, context.medianLatency)
136
- if (lat < minLatency) minLatency = lat
137
- if (lat > maxLatency) maxLatency = lat
138
-
139
- const cap = availableCapacity(c.peer)
140
- if (cap > maxCap) maxCap = cap
141
- }
142
-
143
- const priceRange = maxPrice - minPrice
144
- const latencyRange = maxLatency - minLatency
145
-
146
- // --- Score each candidate ---
147
- const scored: ScoredCandidate[] = []
148
-
149
- for (const c of candidates) {
150
- // Price factor (lower is better, inverted)
151
- const price = c.offer.inputUsdPerMillion ?? fallbackPrice
152
- const priceFactor = normalizedInverted(price, minPrice, priceRange)
153
-
154
- // Latency factor (lower is better, inverted)
155
- const lat = peerLatency(c.metrics, context.medianLatency)
156
- const latencyFactor = normalizedInverted(lat, minLatency, latencyRange)
157
-
158
- // Capacity factor (higher is better)
159
- const capFactor = maxCap > 0
160
- ? availableCapacity(c.peer) / maxCap
161
- : 1
162
-
163
- // Reputation factor (higher is better, normalized 0-100 to 0-1)
164
- const repFactor = effectiveReputation(c.peer) / 100
165
- const fresh = freshnessFactor(c.peer, context.now, context.maxPeerStalenessMs)
166
- const reliability = reliabilityFactor(c.metrics, context.maxFailures)
167
-
168
- const score =
169
- w.price * priceFactor +
170
- w.latency * latencyFactor +
171
- w.capacity * capFactor +
172
- w.reputation * repFactor +
173
- w.freshness * fresh +
174
- w.reliability * reliability
175
-
176
- scored.push({
177
- peer: c.peer,
178
- provider: c.provider,
179
- providerRank: c.providerRank,
180
- offer: c.offer,
181
- score,
182
- })
183
- }
184
-
185
- // Sort by score descending, tie-break by latency then peerId
186
- scored.sort((a, b) => {
187
- if (Math.abs(a.score - b.score) < 1e-9) {
188
- const aMetrics = candidates.find((c) => c.peer.peerId === a.peer.peerId)
189
- const bMetrics = candidates.find((c) => c.peer.peerId === b.peer.peerId)
190
- return tieBreak(a.peer, aMetrics?.metrics.latencyEma, b.peer, bMetrics?.metrics.latencyEma)
191
- }
192
- return b.score - a.score
193
- })
194
-
195
- return scored
196
- }
package/src/tool-hints.ts DELETED
@@ -1,15 +0,0 @@
1
- export interface ToolHint {
2
- name: string
3
- envVar: string
4
- }
5
-
6
- export const WELL_KNOWN_TOOL_HINTS: ToolHint[] = [
7
- { name: 'Claude Code', envVar: 'ANTHROPIC_BASE_URL' },
8
- { name: 'Aider', envVar: 'OPENAI_API_BASE' },
9
- { name: 'Continue.dev', envVar: 'OPENAI_BASE_URL' },
10
- { name: 'Codex', envVar: 'OPENAI_BASE_URL' },
11
- ]
12
-
13
- export function formatToolHints(hints: ToolHint[], proxyUrl: string): string[] {
14
- return hints.map(hint => `export ${hint.envVar}=${proxyUrl} # ${hint.name}`)
15
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "exclude": ["src/**/*.test.ts"]
9
- }