@antseed/router-local-proxy 0.1.0
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 +49 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +45 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +197 -0
- package/dist/router.js.map +1 -0
- package/package.json +27 -0
- package/src/index.ts +167 -0
- package/src/router.test.ts +256 -0
- package/src/router.ts +272 -0
- package/tsconfig.json +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @antseed/router-local-proxy
|
|
2
|
+
|
|
3
|
+
Local HTTP proxy router for AI coding tools. Drop-in replacement for `ANTHROPIC_BASE_URL` or `OPENAI_BASE_URL` that routes requests through the Antseed P2P network.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
antseed plugin add @antseed/router-local-proxy
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
antseed connect --router local-proxy
|
|
15
|
+
|
|
16
|
+
# Then configure your tools:
|
|
17
|
+
export ANTHROPIC_BASE_URL=http://localhost:8377
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Works with Claude Code, Aider, Continue.dev, OpenAI Codex, and any tool that supports custom base URLs.
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
| Key | Type | Required | Default | Description |
|
|
25
|
+
|-----|------|----------|---------|-------------|
|
|
26
|
+
| `ANTSEED_MIN_REPUTATION` | number | No | 50 | Minimum peer reputation (0-100) |
|
|
27
|
+
| `ANTSEED_PREFERRED_PROVIDERS` | string[] | No | -- | Ordered list of preferred providers |
|
|
28
|
+
| `ANTSEED_MAX_PRICING_JSON` | string | No | -- | Max pricing config as JSON |
|
|
29
|
+
| `ANTSEED_MAX_FAILURES` | number | No | 3 | Max failures before cooldown |
|
|
30
|
+
| `ANTSEED_FAILURE_COOLDOWN_MS` | number | No | 30000 | Cooldown duration after failures (ms) |
|
|
31
|
+
| `ANTSEED_MAX_PEER_STALENESS_MS` | number | No | 300000 | Max age of peer info before deprioritizing |
|
|
32
|
+
|
|
33
|
+
## Max Pricing
|
|
34
|
+
|
|
35
|
+
Set maximum prices you're willing to pay:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export ANTSEED_MAX_PRICING_JSON='{"defaults":{"inputUsdPerMillion":20,"outputUsdPerMillion":60}}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or with per-provider overrides:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export ANTSEED_MAX_PRICING_JSON='{"defaults":{"inputUsdPerMillion":20,"outputUsdPerMillion":60},"providers":{"anthropic":{"models":{"claude-sonnet-4-5-20250929":{"inputUsdPerMillion":15,"outputUsdPerMillion":75}}}}}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
Uses `scoreCandidates` and `PeerMetricsTracker` from `@antseed/router-core`. Scores peers on price, latency, capacity, reputation, freshness, and reliability. Enforces max pricing limits and preferred provider ordering. Peers that exceed price limits or fail repeatedly are excluded.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AntseedRouterPlugin } from '@antseed/node';
|
|
2
|
+
import { formatToolHints } from '@antseed/router-core';
|
|
3
|
+
declare const plugin: AntseedRouterPlugin;
|
|
4
|
+
export default plugin;
|
|
5
|
+
export declare const TOOL_HINTS: import("@antseed/router-core").ToolHint[];
|
|
6
|
+
export { formatToolHints };
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAyB,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAsH9E,QAAA,MAAM,MAAM,EAAE,mBA0Cb,CAAC;AAEF,eAAe,MAAM,CAAC;AAEtB,eAAO,MAAM,UAAU,2CAAwB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { WELL_KNOWN_TOOL_HINTS, formatToolHints } from '@antseed/router-core';
|
|
2
|
+
import { LocalProxyRouter } from './router.js';
|
|
3
|
+
function parseCsvProviders(raw) {
|
|
4
|
+
if (!raw)
|
|
5
|
+
return undefined;
|
|
6
|
+
const parsed = raw
|
|
7
|
+
.split(',')
|
|
8
|
+
.map((provider) => provider.trim())
|
|
9
|
+
.filter((provider) => provider.length > 0);
|
|
10
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
11
|
+
}
|
|
12
|
+
function isNonNegativeFinite(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
14
|
+
}
|
|
15
|
+
function parseMaxPricingJson(raw) {
|
|
16
|
+
if (!raw)
|
|
17
|
+
return undefined;
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON must be valid JSON');
|
|
24
|
+
}
|
|
25
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
26
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON must be an object');
|
|
27
|
+
}
|
|
28
|
+
const root = parsed;
|
|
29
|
+
const defaults = root['defaults'];
|
|
30
|
+
if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
31
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.defaults must be an object');
|
|
32
|
+
}
|
|
33
|
+
const input = defaults['inputUsdPerMillion'];
|
|
34
|
+
const output = defaults['outputUsdPerMillion'];
|
|
35
|
+
if (!isNonNegativeFinite(input) || !isNonNegativeFinite(output)) {
|
|
36
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.defaults must include non-negative inputUsdPerMillion/outputUsdPerMillion');
|
|
37
|
+
}
|
|
38
|
+
const result = {
|
|
39
|
+
defaults: {
|
|
40
|
+
inputUsdPerMillion: input,
|
|
41
|
+
outputUsdPerMillion: output,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const providersRaw = root['providers'];
|
|
45
|
+
if (providersRaw !== undefined) {
|
|
46
|
+
if (!providersRaw || typeof providersRaw !== 'object' || Array.isArray(providersRaw)) {
|
|
47
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.providers must be an object');
|
|
48
|
+
}
|
|
49
|
+
const providersOut = {};
|
|
50
|
+
for (const [provider, rawProviderConfig] of Object.entries(providersRaw)) {
|
|
51
|
+
if (!rawProviderConfig || typeof rawProviderConfig !== 'object' || Array.isArray(rawProviderConfig)) {
|
|
52
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider} must be an object`);
|
|
53
|
+
}
|
|
54
|
+
const providerObj = rawProviderConfig;
|
|
55
|
+
const providerOut = {};
|
|
56
|
+
const providerDefaults = providerObj['defaults'];
|
|
57
|
+
if (providerDefaults !== undefined) {
|
|
58
|
+
if (!providerDefaults || typeof providerDefaults !== 'object' || Array.isArray(providerDefaults)) {
|
|
59
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.defaults must be an object`);
|
|
60
|
+
}
|
|
61
|
+
const providerInput = providerDefaults['inputUsdPerMillion'];
|
|
62
|
+
const providerOutput = providerDefaults['outputUsdPerMillion'];
|
|
63
|
+
if (!isNonNegativeFinite(providerInput) || !isNonNegativeFinite(providerOutput)) {
|
|
64
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.defaults must include non-negative inputUsdPerMillion/outputUsdPerMillion`);
|
|
65
|
+
}
|
|
66
|
+
providerOut.defaults = {
|
|
67
|
+
inputUsdPerMillion: providerInput,
|
|
68
|
+
outputUsdPerMillion: providerOutput,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const modelPricing = providerObj['models'];
|
|
72
|
+
if (modelPricing !== undefined) {
|
|
73
|
+
if (!modelPricing || typeof modelPricing !== 'object' || Array.isArray(modelPricing)) {
|
|
74
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models must be an object`);
|
|
75
|
+
}
|
|
76
|
+
const modelsOut = {};
|
|
77
|
+
for (const [model, modelPricingRaw] of Object.entries(modelPricing)) {
|
|
78
|
+
if (!modelPricingRaw || typeof modelPricingRaw !== 'object' || Array.isArray(modelPricingRaw)) {
|
|
79
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models.${model} must be an object`);
|
|
80
|
+
}
|
|
81
|
+
const modelInput = modelPricingRaw['inputUsdPerMillion'];
|
|
82
|
+
const modelOutput = modelPricingRaw['outputUsdPerMillion'];
|
|
83
|
+
if (!isNonNegativeFinite(modelInput) || !isNonNegativeFinite(modelOutput)) {
|
|
84
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models.${model} must include non-negative inputUsdPerMillion/outputUsdPerMillion`);
|
|
85
|
+
}
|
|
86
|
+
modelsOut[model] = {
|
|
87
|
+
inputUsdPerMillion: modelInput,
|
|
88
|
+
outputUsdPerMillion: modelOutput,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (Object.keys(modelsOut).length > 0) {
|
|
92
|
+
providerOut.models = modelsOut;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
providersOut[provider] = providerOut;
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(providersOut).length > 0) {
|
|
98
|
+
result.providers = providersOut;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
const plugin = {
|
|
104
|
+
name: 'local-proxy',
|
|
105
|
+
displayName: 'Local Proxy',
|
|
106
|
+
version: '0.1.0',
|
|
107
|
+
type: 'router',
|
|
108
|
+
description: 'Local proxy router for Claude Code, Aider, Continue.dev, OpenAI Codex',
|
|
109
|
+
configSchema: [
|
|
110
|
+
{ key: 'ANTSEED_MIN_REPUTATION', label: 'Min Reputation', type: 'number', required: false, default: 50, description: 'Min peer reputation 0-100' },
|
|
111
|
+
{ key: 'ANTSEED_PREFERRED_PROVIDERS', label: 'Preferred Providers', type: 'string[]', required: false, description: 'Ordered preferred providers' },
|
|
112
|
+
{ key: 'ANTSEED_MAX_PRICING_JSON', label: 'Max Pricing JSON', type: 'string', required: false, description: 'Buyer max pricing JSON' },
|
|
113
|
+
{ key: 'ANTSEED_MAX_FAILURES', label: 'Max Failures', type: 'number', required: false, default: 3, description: 'Max consecutive failures before excluding peer' },
|
|
114
|
+
{ key: 'ANTSEED_FAILURE_COOLDOWN_MS', label: 'Failure Cooldown (ms)', type: 'number', required: false, default: 30000, description: 'Cooldown after repeated failures (ms)' },
|
|
115
|
+
{ key: 'ANTSEED_MAX_PEER_STALENESS_MS', label: 'Max Peer Staleness (ms)', type: 'number', required: false, default: 300000, description: 'Peer staleness horizon (ms)' },
|
|
116
|
+
],
|
|
117
|
+
createRouter(config) {
|
|
118
|
+
const minReputation = config['ANTSEED_MIN_REPUTATION'] ? parseInt(config['ANTSEED_MIN_REPUTATION'], 10) : undefined;
|
|
119
|
+
if (minReputation !== undefined && Number.isNaN(minReputation)) {
|
|
120
|
+
throw new Error('ANTSEED_MIN_REPUTATION must be a valid number');
|
|
121
|
+
}
|
|
122
|
+
const preferredProviders = parseCsvProviders(config['ANTSEED_PREFERRED_PROVIDERS']);
|
|
123
|
+
const maxPricing = parseMaxPricingJson(config['ANTSEED_MAX_PRICING_JSON']);
|
|
124
|
+
const maxFailures = config['ANTSEED_MAX_FAILURES'] ? parseInt(config['ANTSEED_MAX_FAILURES'], 10) : undefined;
|
|
125
|
+
if (maxFailures !== undefined && Number.isNaN(maxFailures)) {
|
|
126
|
+
throw new Error('ANTSEED_MAX_FAILURES must be a valid number');
|
|
127
|
+
}
|
|
128
|
+
const failureCooldownMs = config['ANTSEED_FAILURE_COOLDOWN_MS'] ? parseInt(config['ANTSEED_FAILURE_COOLDOWN_MS'], 10) : undefined;
|
|
129
|
+
if (failureCooldownMs !== undefined && Number.isNaN(failureCooldownMs)) {
|
|
130
|
+
throw new Error('ANTSEED_FAILURE_COOLDOWN_MS must be a valid number');
|
|
131
|
+
}
|
|
132
|
+
const maxPeerStalenessMs = config['ANTSEED_MAX_PEER_STALENESS_MS'] ? parseInt(config['ANTSEED_MAX_PEER_STALENESS_MS'], 10) : undefined;
|
|
133
|
+
if (maxPeerStalenessMs !== undefined && Number.isNaN(maxPeerStalenessMs)) {
|
|
134
|
+
throw new Error('ANTSEED_MAX_PEER_STALENESS_MS must be a valid number');
|
|
135
|
+
}
|
|
136
|
+
return new LocalProxyRouter({
|
|
137
|
+
preferredProviders,
|
|
138
|
+
minReputation,
|
|
139
|
+
maxPricing,
|
|
140
|
+
maxFailures,
|
|
141
|
+
failureCooldownMs,
|
|
142
|
+
maxPeerStalenessMs,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
export default plugin;
|
|
147
|
+
export const TOOL_HINTS = WELL_KNOWN_TOOL_HINTS;
|
|
148
|
+
export { formatToolHints };
|
|
149
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Router, PeerInfo, SerializedHttpRequest } from '@antseed/node';
|
|
2
|
+
import { type TokenPricingUsdPerMillion, type ScoringWeights } from '@antseed/router-core';
|
|
3
|
+
export interface BuyerMaxPricingConfig {
|
|
4
|
+
defaults: TokenPricingUsdPerMillion;
|
|
5
|
+
providers?: Record<string, {
|
|
6
|
+
defaults?: TokenPricingUsdPerMillion;
|
|
7
|
+
models?: Record<string, TokenPricingUsdPerMillion>;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface LocalProxyRouterConfig {
|
|
11
|
+
preferredProviders?: string[];
|
|
12
|
+
minReputation?: number;
|
|
13
|
+
maxPricing?: BuyerMaxPricingConfig;
|
|
14
|
+
maxFailures?: number;
|
|
15
|
+
failureCooldownMs?: number;
|
|
16
|
+
maxPeerStalenessMs?: number;
|
|
17
|
+
weights?: Partial<ScoringWeights>;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare class LocalProxyRouter implements Router {
|
|
21
|
+
private readonly _preferredProviders;
|
|
22
|
+
private readonly _minReputation;
|
|
23
|
+
private readonly _maxPricing;
|
|
24
|
+
private readonly _maxFailures;
|
|
25
|
+
private readonly _maxPeerStalenessMs;
|
|
26
|
+
private readonly _now;
|
|
27
|
+
private readonly _weights;
|
|
28
|
+
private readonly _metrics;
|
|
29
|
+
constructor(config?: LocalProxyRouterConfig);
|
|
30
|
+
selectPeer(req: SerializedHttpRequest, peers: PeerInfo[]): PeerInfo | null;
|
|
31
|
+
onResult(peer: PeerInfo, result: {
|
|
32
|
+
success: boolean;
|
|
33
|
+
latencyMs: number;
|
|
34
|
+
tokens: number;
|
|
35
|
+
}): void;
|
|
36
|
+
private _effectiveReputation;
|
|
37
|
+
private _hasReputation;
|
|
38
|
+
private _extractRequestedModel;
|
|
39
|
+
private _selectProviderForPeer;
|
|
40
|
+
private _resolvePeerOfferPrice;
|
|
41
|
+
private _resolveBuyerMaxPrice;
|
|
42
|
+
private _isFiniteNonNegative;
|
|
43
|
+
private _isValidOffer;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,yBAAyB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QACzB,QAAQ,CAAC,EAAE,yBAAyB,CAAC;QACrC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;KACpD,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,sBAAsB;IACrC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,qBAAqB,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAiB,YAAW,MAAM;IAC7C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAW;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,MAAM,CAAC,EAAE,sBAAsB;IAuB3C,UAAU,CAAC,GAAG,EAAE,qBAAqB,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,GAAG,IAAI;IAmF1E,QAAQ,CACN,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAC9D,IAAI;IAOP,OAAO,CAAC,oBAAoB;IAO5B,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,sBAAsB;IAkB9B,OAAO,CAAC,sBAAsB;IAoB9B,OAAO,CAAC,sBAAsB;IAgC9B,OAAO,CAAC,qBAAqB;IAkB7B,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,aAAa;CAMtB"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { scoreCandidates, PeerMetricsTracker, } from '@antseed/router-core';
|
|
2
|
+
export class LocalProxyRouter {
|
|
3
|
+
_preferredProviders;
|
|
4
|
+
_minReputation;
|
|
5
|
+
_maxPricing;
|
|
6
|
+
_maxFailures;
|
|
7
|
+
_maxPeerStalenessMs;
|
|
8
|
+
_now;
|
|
9
|
+
_weights;
|
|
10
|
+
_metrics;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this._preferredProviders = (config?.preferredProviders ?? [])
|
|
13
|
+
.map((provider) => provider.trim())
|
|
14
|
+
.filter((provider) => provider.length > 0);
|
|
15
|
+
this._minReputation = config?.minReputation ?? 50;
|
|
16
|
+
this._maxPricing = {
|
|
17
|
+
defaults: {
|
|
18
|
+
inputUsdPerMillion: config?.maxPricing?.defaults.inputUsdPerMillion ?? Number.POSITIVE_INFINITY,
|
|
19
|
+
outputUsdPerMillion: config?.maxPricing?.defaults.outputUsdPerMillion ?? Number.POSITIVE_INFINITY,
|
|
20
|
+
},
|
|
21
|
+
...(config?.maxPricing?.providers ? { providers: config.maxPricing.providers } : {}),
|
|
22
|
+
};
|
|
23
|
+
this._maxFailures = Math.max(1, config?.maxFailures ?? 3);
|
|
24
|
+
this._maxPeerStalenessMs = Math.max(1, config?.maxPeerStalenessMs ?? 300_000);
|
|
25
|
+
this._now = config?.now ?? (() => Date.now());
|
|
26
|
+
this._weights = config?.weights;
|
|
27
|
+
this._metrics = new PeerMetricsTracker({
|
|
28
|
+
maxFailures: this._maxFailures,
|
|
29
|
+
failureCooldownMs: Math.max(1, config?.failureCooldownMs ?? 30_000),
|
|
30
|
+
now: this._now,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
selectPeer(req, peers) {
|
|
34
|
+
const now = this._now();
|
|
35
|
+
const requestedModel = this._extractRequestedModel(req);
|
|
36
|
+
const candidates = [];
|
|
37
|
+
for (const peer of peers) {
|
|
38
|
+
// Reputation filter
|
|
39
|
+
if (this._hasReputation(peer)) {
|
|
40
|
+
const reputation = this._effectiveReputation(peer);
|
|
41
|
+
if (reputation < this._minReputation) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Cooldown filter
|
|
46
|
+
if (this._metrics.isCoolingDown(peer.peerId)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Provider availability filter
|
|
50
|
+
const selectedProvider = this._selectProviderForPeer(peer);
|
|
51
|
+
if (!selectedProvider) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Pricing filter
|
|
55
|
+
const offer = this._resolvePeerOfferPrice(peer, selectedProvider.provider, requestedModel);
|
|
56
|
+
if (!offer) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const max = this._resolveBuyerMaxPrice(selectedProvider.provider, requestedModel);
|
|
60
|
+
if (offer.inputUsdPerMillion > max.inputUsdPerMillion || offer.outputUsdPerMillion > max.outputUsdPerMillion) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
candidates.push({
|
|
64
|
+
peer,
|
|
65
|
+
provider: selectedProvider.provider,
|
|
66
|
+
providerRank: selectedProvider.rank,
|
|
67
|
+
offer,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (candidates.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
// Provider preference filtering
|
|
73
|
+
let providerFiltered = candidates;
|
|
74
|
+
if (this._preferredProviders.length > 0) {
|
|
75
|
+
const bestRank = Math.min(...candidates.map((c) => c.providerRank));
|
|
76
|
+
providerFiltered = candidates.filter((c) => c.providerRank === bestRank);
|
|
77
|
+
}
|
|
78
|
+
if (providerFiltered.length === 1) {
|
|
79
|
+
return providerFiltered[0].peer;
|
|
80
|
+
}
|
|
81
|
+
// Delegate scoring to router-core
|
|
82
|
+
const scoringInput = providerFiltered.map((c) => ({
|
|
83
|
+
peer: c.peer,
|
|
84
|
+
provider: c.provider,
|
|
85
|
+
providerRank: c.providerRank,
|
|
86
|
+
offer: c.offer,
|
|
87
|
+
metrics: this._metrics.getMetrics(c.peer.peerId),
|
|
88
|
+
}));
|
|
89
|
+
const scored = scoreCandidates(scoringInput, {
|
|
90
|
+
now,
|
|
91
|
+
medianLatency: this._metrics.getMedianLatency(),
|
|
92
|
+
maxPeerStalenessMs: this._maxPeerStalenessMs,
|
|
93
|
+
maxFailures: this._maxFailures,
|
|
94
|
+
weights: this._weights,
|
|
95
|
+
});
|
|
96
|
+
return scored[0]?.peer ?? null;
|
|
97
|
+
}
|
|
98
|
+
onResult(peer, result) {
|
|
99
|
+
this._metrics.recordResult(peer.peerId, {
|
|
100
|
+
success: result.success,
|
|
101
|
+
latencyMs: result.latencyMs,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
_effectiveReputation(p) {
|
|
105
|
+
if (p.onChainReputation !== undefined) {
|
|
106
|
+
return p.onChainReputation;
|
|
107
|
+
}
|
|
108
|
+
return p.trustScore ?? p.reputationScore ?? 0;
|
|
109
|
+
}
|
|
110
|
+
_hasReputation(p) {
|
|
111
|
+
if (this._isFiniteNonNegative(p.onChainReputation)) {
|
|
112
|
+
const sessionCount = this._isFiniteNonNegative(p.onChainSessionCount) ? p.onChainSessionCount : undefined;
|
|
113
|
+
const disputeCount = this._isFiniteNonNegative(p.onChainDisputeCount) ? p.onChainDisputeCount : undefined;
|
|
114
|
+
if (sessionCount !== undefined || disputeCount !== undefined) {
|
|
115
|
+
return (sessionCount ?? 0) > 0 || (disputeCount ?? 0) > 0;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
return this._isFiniteNonNegative(p.trustScore) || this._isFiniteNonNegative(p.reputationScore);
|
|
120
|
+
}
|
|
121
|
+
_extractRequestedModel(req) {
|
|
122
|
+
const contentType = req.headers['content-type'] ?? req.headers['Content-Type'] ?? '';
|
|
123
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(new TextDecoder().decode(req.body));
|
|
128
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const model = parsed['model'];
|
|
132
|
+
return typeof model === 'string' && model.trim().length > 0 ? model.trim() : null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
_selectProviderForPeer(peer) {
|
|
139
|
+
const availableProviders = peer.providers
|
|
140
|
+
.map((provider) => provider.trim())
|
|
141
|
+
.filter((provider) => provider.length > 0);
|
|
142
|
+
if (this._preferredProviders.length === 0) {
|
|
143
|
+
const provider = availableProviders[0];
|
|
144
|
+
return provider ? { provider, rank: Number.MAX_SAFE_INTEGER } : null;
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < this._preferredProviders.length; i++) {
|
|
147
|
+
const preferred = this._preferredProviders[i];
|
|
148
|
+
if (availableProviders.includes(preferred)) {
|
|
149
|
+
return { provider: preferred, rank: i };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
_resolvePeerOfferPrice(peer, provider, model) {
|
|
155
|
+
const providerPricing = peer.providerPricing?.[provider];
|
|
156
|
+
if (model) {
|
|
157
|
+
const modelSpecific = providerPricing?.models?.[model];
|
|
158
|
+
if (modelSpecific && this._isValidOffer(modelSpecific)) {
|
|
159
|
+
return modelSpecific;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const providerDefaults = providerPricing?.defaults;
|
|
163
|
+
if (providerDefaults && this._isValidOffer(providerDefaults)) {
|
|
164
|
+
return providerDefaults;
|
|
165
|
+
}
|
|
166
|
+
if (this._isFiniteNonNegative(peer.defaultInputUsdPerMillion) &&
|
|
167
|
+
this._isFiniteNonNegative(peer.defaultOutputUsdPerMillion)) {
|
|
168
|
+
return {
|
|
169
|
+
inputUsdPerMillion: peer.defaultInputUsdPerMillion,
|
|
170
|
+
outputUsdPerMillion: peer.defaultOutputUsdPerMillion,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
_resolveBuyerMaxPrice(provider, model) {
|
|
176
|
+
const providerPricing = this._maxPricing.providers?.[provider];
|
|
177
|
+
if (model) {
|
|
178
|
+
const modelOverride = providerPricing?.models?.[model];
|
|
179
|
+
if (modelOverride && this._isValidOffer(modelOverride)) {
|
|
180
|
+
return modelOverride;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const providerDefaults = providerPricing?.defaults;
|
|
184
|
+
if (providerDefaults && this._isValidOffer(providerDefaults)) {
|
|
185
|
+
return providerDefaults;
|
|
186
|
+
}
|
|
187
|
+
return this._maxPricing.defaults;
|
|
188
|
+
}
|
|
189
|
+
_isFiniteNonNegative(value) {
|
|
190
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
191
|
+
}
|
|
192
|
+
_isValidOffer(offer) {
|
|
193
|
+
return (this._isFiniteNonNegative(offer.inputUsdPerMillion) &&
|
|
194
|
+
this._isFiniteNonNegative(offer.outputUsdPerMillion));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EACf,kBAAkB,GAGnB,MAAM,sBAAsB,CAAC;AAqB9B,MAAM,OAAO,gBAAgB;IACV,mBAAmB,CAAW;IAC9B,cAAc,CAAS;IACvB,WAAW,CAAwB;IACnC,YAAY,CAAS;IACrB,mBAAmB,CAAS;IAC5B,IAAI,CAAe;IACnB,QAAQ,CAAsC;IAC9C,QAAQ,CAAqB;IAE9C,YAAY,MAA+B;QACzC,IAAI,CAAC,mBAAmB,GAAG,CAAC,MAAM,EAAE,kBAAkB,IAAI,EAAE,CAAC;aAC1D,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;aAClC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC7C,IAAI,CAAC,cAAc,GAAG,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG;YACjB,QAAQ,EAAE;gBACR,kBAAkB,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,kBAAkB,IAAI,MAAM,CAAC,iBAAiB;gBAC/F,mBAAmB,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,mBAAmB,IAAI,MAAM,CAAC,iBAAiB;aAClG;YACD,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrF,CAAC;QACF,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,IAAI,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB,IAAI,OAAO,CAAC,CAAC;QAC9E,IAAI,CAAC,IAAI,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,GAAG,MAAM,EAAE,OAAO,CAAC;QAChC,IAAI,CAAC,QAAQ,GAAG,IAAI,kBAAkB,CAAC;YACrC,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,IAAI,MAAM,CAAC;YACnE,GAAG,EAAE,IAAI,CAAC,IAAI;SACf,CAAC,CAAC;IACL,CAAC;IAED,UAAU,CAAC,GAA0B,EAAE,KAAiB;QACtD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,cAAc,GAAG,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAExD,MAAM,UAAU,GAKV,EAAE,CAAC;QAET,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,oBAAoB;YACpB,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC;gBACnD,IAAI,UAAU,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;oBACrC,SAAS;gBACX,CAAC;YACH,CAAC;YAED,kBAAkB;YAClB,IAAI,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC7C,SAAS;YACX,CAAC;YAED,+BAA+B;YAC/B,MAAM,gBAAgB,GAAG,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;YAC3D,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACtB,SAAS;YACX,CAAC;YAED,iBAAiB;YACjB,MAAM,KAAK,GAAG,IAAI,CAAC,sBAAsB,CAAC,IAAI,EAAE,gBAAgB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;YAC3F,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,SAAS;YACX,CAAC;YAED,MAAM,GAAG,GAAG,IAAI,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;YAClF,IAAI,KAAK,CAAC,kBAAkB,GAAG,GAAG,CAAC,kBAAkB,IAAI,KAAK,CAAC,mBAAmB,GAAG,GAAG,CAAC,mBAAmB,EAAE,CAAC;gBAC7G,SAAS;YACX,CAAC;YAED,UAAU,CAAC,IAAI,CAAC;gBACd,IAAI;gBACJ,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;gBACnC,YAAY,EAAE,gBAAgB,CAAC,IAAI;gBACnC,KAAK;aACN,CAAC,CAAC;QACL,CAAC;QAED,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAEzC,gCAAgC;QAChC,IAAI,gBAAgB,GAAG,UAAU,CAAC;QAClC,IAAI,IAAI,CAAC,mBAAmB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;YACpE,gBAAgB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC;QAC3E,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClC,OAAO,gBAAgB,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC;QACnC,CAAC;QAED,kCAAkC;QAClC,MAAM,YAAY,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAChD,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;YACpB,YAAY,EAAE,CAAC,CAAC,YAAY;YAC5B,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;SACjD,CAAC,CAAC,CAAC;QAEJ,MAAM,MAAM,GAAG,eAAe,CAAC,YAAY,EAAE;YAC3C,GAAG;YACH,aAAa,EAAE,IAAI,CAAC,QAAQ,CAAC,gBAAgB,EAAE;YAC/C,kBAAkB,EAAE,IAAI,CAAC,mBAAmB;YAC5C,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,OAAO,EAAE,IAAI,CAAC,QAAQ;SACvB,CAAC,CAAC;QAEH,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;IACjC,CAAC;IAED,QAAQ,CACN,IAAc,EACd,MAA+D;QAE/D,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,EAAE;YACtC,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC,CAAC;IACL,CAAC;IAEO,oBAAoB,CAAC,CAAW;QACtC,IAAI,CAAC,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;YACtC,OAAO,CAAC,CAAC,iBAAiB,CAAC;QAC7B,CAAC;QACD,OAAO,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC;IAChD,CAAC;IAEO,cAAc,CAAC,CAAW;QAChC,IAAI,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACnD,MAAM,YAAY,GAAG,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1G,MAAM,YAAY,GAAG,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC;YAC1G,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC7D,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAC5D,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;IACjG,CAAC;IAEO,sBAAsB,CAAC,GAA0B;QACvD,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QACrF,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC5D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAY,CAAC;YACzE,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAC1C,OAAO,IAAI,CAAC;YACd,CAAC;YACD,MAAM,KAAK,GAAI,MAAkC,CAAC,OAAO,CAAC,CAAC;YAC3D,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACpF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,sBAAsB,CAAC,IAAc;QAC3C,MAAM,kBAAkB,GAAG,IAAI,CAAC,SAAS;aACtC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;aAClC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAE7C,IAAI,IAAI,CAAC,mBAAmB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;YACvC,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,CAAC;QAED,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACzD,MAAM,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAE,CAAC;YAC/C,IAAI,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC3C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAC1C,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,sBAAsB,CAC5B,IAAc,EACd,QAAgB,EAChB,KAAoB;QAEpB,MAAM,eAAe,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC,QAAQ,CAAC,CAAC;QAEzD,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,aAAa,GAAG,eAAe,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;YACvD,IAAI,aAAa,IAAI,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;gBACvD,OAAO,aAAa,CAAC;YACvB,CAAC;QACH,CAAC;QAED,MAAM,gBAAgB,GAAG,eAAe,EAAE,QAAQ,CAAC;QACnD,IAAI,gBAAgB,IAAI,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC7D,OAAO,gBAAgB,CAAC;QAC1B,CAAC;QAED,IACE,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,yBAAyB,CAAC;YACzD,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,0BAA0B,CAAC,EAC1D,CAAC;YACD,OAAO;gBACL,kBAAkB,EAAE,IAAI,CAAC,yBAAyB;gBAClD,mBAAmB,EAAE,IAAI,CAAC,0BAA0B;aACrD,CAAC;QACJ,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,qBAAqB,CAAC,QAAgB,EAAE,KAAoB;QAClE,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,CAAC,QAAQ,CAAC,CAAC;QAE/D,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,aAAa,GAAG,eAAe,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;YACvD,IAAI,aAAa,IAAI,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC;gBACvD,OAAO,aAAa,CAAC;YACvB,CAAC;QACH,CAAC;QAED,MAAM,gBAAgB,GAAG,eAAe,EAAE,QAAQ,CAAC;QACnD,IAAI,gBAAgB,IAAI,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC7D,OAAO,gBAAgB,CAAC;QAC1B,CAAC;QAED,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;IACnC,CAAC;IAEO,oBAAoB,CAAC,KAAyB;QACpD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC3E,CAAC;IAEO,aAAa,CAAC,KAAgC;QACpD,OAAO,CACL,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,kBAAkB,CAAC;YACnD,IAAI,CAAC,oBAAoB,CAAC,KAAK,CAAC,mBAAmB,CAAC,CACrD,CAAC;IACJ,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@antseed/router-local-proxy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Antseed local proxy router plugin — Claude Code, Aider, Continue.dev",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"test": "vitest run"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@antseed/router-core": "workspace:*"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@antseed/node": ">=0.1.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.5.0",
|
|
24
|
+
"vitest": "^2.0.0",
|
|
25
|
+
"@antseed/node": "workspace:*"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { AntseedRouterPlugin } from '@antseed/node';
|
|
2
|
+
import { WELL_KNOWN_TOOL_HINTS, formatToolHints } from '@antseed/router-core';
|
|
3
|
+
import { LocalProxyRouter, type BuyerMaxPricingConfig } from './router.js';
|
|
4
|
+
|
|
5
|
+
function parseCsvProviders(raw: string | undefined): string[] | undefined {
|
|
6
|
+
if (!raw) return undefined;
|
|
7
|
+
const parsed = raw
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((provider) => provider.trim())
|
|
10
|
+
.filter((provider) => provider.length > 0);
|
|
11
|
+
return parsed.length > 0 ? parsed : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isNonNegativeFinite(value: unknown): value is number {
|
|
15
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseMaxPricingJson(raw: string | undefined): BuyerMaxPricingConfig | undefined {
|
|
19
|
+
if (!raw) return undefined;
|
|
20
|
+
|
|
21
|
+
let parsed: unknown;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw) as unknown;
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON must be valid JSON');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON must be an object');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const root = parsed as Record<string, unknown>;
|
|
33
|
+
const defaults = root['defaults'];
|
|
34
|
+
if (!defaults || typeof defaults !== 'object' || Array.isArray(defaults)) {
|
|
35
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.defaults must be an object');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const input = (defaults as Record<string, unknown>)['inputUsdPerMillion'];
|
|
39
|
+
const output = (defaults as Record<string, unknown>)['outputUsdPerMillion'];
|
|
40
|
+
if (!isNonNegativeFinite(input) || !isNonNegativeFinite(output)) {
|
|
41
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.defaults must include non-negative inputUsdPerMillion/outputUsdPerMillion');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result: BuyerMaxPricingConfig = {
|
|
45
|
+
defaults: {
|
|
46
|
+
inputUsdPerMillion: input,
|
|
47
|
+
outputUsdPerMillion: output,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const providersRaw = root['providers'];
|
|
52
|
+
if (providersRaw !== undefined) {
|
|
53
|
+
if (!providersRaw || typeof providersRaw !== 'object' || Array.isArray(providersRaw)) {
|
|
54
|
+
throw new Error('ANTSEED_MAX_PRICING_JSON.providers must be an object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const providersOut: NonNullable<BuyerMaxPricingConfig['providers']> = {};
|
|
58
|
+
for (const [provider, rawProviderConfig] of Object.entries(providersRaw as Record<string, unknown>)) {
|
|
59
|
+
if (!rawProviderConfig || typeof rawProviderConfig !== 'object' || Array.isArray(rawProviderConfig)) {
|
|
60
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider} must be an object`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const providerObj = rawProviderConfig as Record<string, unknown>;
|
|
64
|
+
const providerOut: NonNullable<BuyerMaxPricingConfig['providers']>[string] = {};
|
|
65
|
+
|
|
66
|
+
const providerDefaults = providerObj['defaults'];
|
|
67
|
+
if (providerDefaults !== undefined) {
|
|
68
|
+
if (!providerDefaults || typeof providerDefaults !== 'object' || Array.isArray(providerDefaults)) {
|
|
69
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.defaults must be an object`);
|
|
70
|
+
}
|
|
71
|
+
const providerInput = (providerDefaults as Record<string, unknown>)['inputUsdPerMillion'];
|
|
72
|
+
const providerOutput = (providerDefaults as Record<string, unknown>)['outputUsdPerMillion'];
|
|
73
|
+
if (!isNonNegativeFinite(providerInput) || !isNonNegativeFinite(providerOutput)) {
|
|
74
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.defaults must include non-negative inputUsdPerMillion/outputUsdPerMillion`);
|
|
75
|
+
}
|
|
76
|
+
providerOut.defaults = {
|
|
77
|
+
inputUsdPerMillion: providerInput,
|
|
78
|
+
outputUsdPerMillion: providerOutput,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const modelPricing = providerObj['models'];
|
|
83
|
+
if (modelPricing !== undefined) {
|
|
84
|
+
if (!modelPricing || typeof modelPricing !== 'object' || Array.isArray(modelPricing)) {
|
|
85
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models must be an object`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const modelsOut: NonNullable<BuyerMaxPricingConfig['providers']>[string]['models'] = {};
|
|
89
|
+
for (const [model, modelPricingRaw] of Object.entries(modelPricing as Record<string, unknown>)) {
|
|
90
|
+
if (!modelPricingRaw || typeof modelPricingRaw !== 'object' || Array.isArray(modelPricingRaw)) {
|
|
91
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models.${model} must be an object`);
|
|
92
|
+
}
|
|
93
|
+
const modelInput = (modelPricingRaw as Record<string, unknown>)['inputUsdPerMillion'];
|
|
94
|
+
const modelOutput = (modelPricingRaw as Record<string, unknown>)['outputUsdPerMillion'];
|
|
95
|
+
if (!isNonNegativeFinite(modelInput) || !isNonNegativeFinite(modelOutput)) {
|
|
96
|
+
throw new Error(`ANTSEED_MAX_PRICING_JSON.providers.${provider}.models.${model} must include non-negative inputUsdPerMillion/outputUsdPerMillion`);
|
|
97
|
+
}
|
|
98
|
+
modelsOut[model] = {
|
|
99
|
+
inputUsdPerMillion: modelInput,
|
|
100
|
+
outputUsdPerMillion: modelOutput,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Object.keys(modelsOut).length > 0) {
|
|
105
|
+
providerOut.models = modelsOut;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
providersOut[provider] = providerOut;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Object.keys(providersOut).length > 0) {
|
|
113
|
+
result.providers = providersOut;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const plugin: AntseedRouterPlugin = {
|
|
121
|
+
name: 'local-proxy',
|
|
122
|
+
displayName: 'Local Proxy',
|
|
123
|
+
version: '0.1.0',
|
|
124
|
+
type: 'router',
|
|
125
|
+
description: 'Local proxy router for Claude Code, Aider, Continue.dev, OpenAI Codex',
|
|
126
|
+
configSchema: [
|
|
127
|
+
{ key: 'ANTSEED_MIN_REPUTATION', label: 'Min Reputation', type: 'number', required: false, default: 50, description: 'Min peer reputation 0-100' },
|
|
128
|
+
{ key: 'ANTSEED_PREFERRED_PROVIDERS', label: 'Preferred Providers', type: 'string[]', required: false, description: 'Ordered preferred providers' },
|
|
129
|
+
{ key: 'ANTSEED_MAX_PRICING_JSON', label: 'Max Pricing JSON', type: 'string', required: false, description: 'Buyer max pricing JSON' },
|
|
130
|
+
{ key: 'ANTSEED_MAX_FAILURES', label: 'Max Failures', type: 'number', required: false, default: 3, description: 'Max consecutive failures before excluding peer' },
|
|
131
|
+
{ key: 'ANTSEED_FAILURE_COOLDOWN_MS', label: 'Failure Cooldown (ms)', type: 'number', required: false, default: 30000, description: 'Cooldown after repeated failures (ms)' },
|
|
132
|
+
{ key: 'ANTSEED_MAX_PEER_STALENESS_MS', label: 'Max Peer Staleness (ms)', type: 'number', required: false, default: 300000, description: 'Peer staleness horizon (ms)' },
|
|
133
|
+
],
|
|
134
|
+
createRouter(config: Record<string, string>) {
|
|
135
|
+
const minReputation = config['ANTSEED_MIN_REPUTATION'] ? parseInt(config['ANTSEED_MIN_REPUTATION'], 10) : undefined;
|
|
136
|
+
if (minReputation !== undefined && Number.isNaN(minReputation)) {
|
|
137
|
+
throw new Error('ANTSEED_MIN_REPUTATION must be a valid number');
|
|
138
|
+
}
|
|
139
|
+
const preferredProviders = parseCsvProviders(config['ANTSEED_PREFERRED_PROVIDERS']);
|
|
140
|
+
const maxPricing = parseMaxPricingJson(config['ANTSEED_MAX_PRICING_JSON']);
|
|
141
|
+
const maxFailures = config['ANTSEED_MAX_FAILURES'] ? parseInt(config['ANTSEED_MAX_FAILURES'], 10) : undefined;
|
|
142
|
+
if (maxFailures !== undefined && Number.isNaN(maxFailures)) {
|
|
143
|
+
throw new Error('ANTSEED_MAX_FAILURES must be a valid number');
|
|
144
|
+
}
|
|
145
|
+
const failureCooldownMs = config['ANTSEED_FAILURE_COOLDOWN_MS'] ? parseInt(config['ANTSEED_FAILURE_COOLDOWN_MS'], 10) : undefined;
|
|
146
|
+
if (failureCooldownMs !== undefined && Number.isNaN(failureCooldownMs)) {
|
|
147
|
+
throw new Error('ANTSEED_FAILURE_COOLDOWN_MS must be a valid number');
|
|
148
|
+
}
|
|
149
|
+
const maxPeerStalenessMs = config['ANTSEED_MAX_PEER_STALENESS_MS'] ? parseInt(config['ANTSEED_MAX_PEER_STALENESS_MS'], 10) : undefined;
|
|
150
|
+
if (maxPeerStalenessMs !== undefined && Number.isNaN(maxPeerStalenessMs)) {
|
|
151
|
+
throw new Error('ANTSEED_MAX_PEER_STALENESS_MS must be a valid number');
|
|
152
|
+
}
|
|
153
|
+
return new LocalProxyRouter({
|
|
154
|
+
preferredProviders,
|
|
155
|
+
minReputation,
|
|
156
|
+
maxPricing,
|
|
157
|
+
maxFailures,
|
|
158
|
+
failureCooldownMs,
|
|
159
|
+
maxPeerStalenessMs,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export default plugin;
|
|
165
|
+
|
|
166
|
+
export const TOOL_HINTS = WELL_KNOWN_TOOL_HINTS;
|
|
167
|
+
export { formatToolHints };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { PeerInfo, SerializedHttpRequest } from '@antseed/node';
|
|
3
|
+
import { LocalProxyRouter } from './router.js';
|
|
4
|
+
|
|
5
|
+
function makePeer(overrides?: Partial<PeerInfo>): PeerInfo {
|
|
6
|
+
return {
|
|
7
|
+
peerId: 'a'.repeat(64) as PeerInfo['peerId'],
|
|
8
|
+
lastSeen: Date.now(),
|
|
9
|
+
providers: ['anthropic'],
|
|
10
|
+
reputationScore: 80,
|
|
11
|
+
trustScore: 80,
|
|
12
|
+
defaultInputUsdPerMillion: 10,
|
|
13
|
+
defaultOutputUsdPerMillion: 10,
|
|
14
|
+
providerPricing: {
|
|
15
|
+
anthropic: {
|
|
16
|
+
defaults: {
|
|
17
|
+
inputUsdPerMillion: 10,
|
|
18
|
+
outputUsdPerMillion: 10,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
maxConcurrency: 10,
|
|
23
|
+
currentLoad: 1,
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRequest(model?: string): SerializedHttpRequest {
|
|
29
|
+
const payload = model ? { model } : { messages: [{ role: 'user', content: 'hi' }] };
|
|
30
|
+
return {
|
|
31
|
+
requestId: 'req-1',
|
|
32
|
+
method: 'POST',
|
|
33
|
+
path: '/v1/messages',
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
body: new TextEncoder().encode(JSON.stringify(payload)),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('LocalProxyRouter', () => {
|
|
40
|
+
it('enforces ordered provider preferences even when lower-ranked provider is cheaper', () => {
|
|
41
|
+
const router = new LocalProxyRouter({
|
|
42
|
+
preferredProviders: ['anthropic', 'openai'],
|
|
43
|
+
maxPricing: {
|
|
44
|
+
defaults: { inputUsdPerMillion: 1_000, outputUsdPerMillion: 1_000 },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const preferredButExpensive = makePeer({
|
|
49
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
50
|
+
providers: ['anthropic'],
|
|
51
|
+
providerPricing: {
|
|
52
|
+
anthropic: {
|
|
53
|
+
defaults: { inputUsdPerMillion: 100, outputUsdPerMillion: 100 },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultInputUsdPerMillion: 100,
|
|
57
|
+
defaultOutputUsdPerMillion: 100,
|
|
58
|
+
});
|
|
59
|
+
const cheaperLowerRank = makePeer({
|
|
60
|
+
peerId: '2'.repeat(64) as PeerInfo['peerId'],
|
|
61
|
+
providers: ['openai'],
|
|
62
|
+
providerPricing: {
|
|
63
|
+
openai: {
|
|
64
|
+
defaults: { inputUsdPerMillion: 1, outputUsdPerMillion: 1 },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
defaultInputUsdPerMillion: 1,
|
|
68
|
+
defaultOutputUsdPerMillion: 1,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const selected = router.selectPeer(makeRequest('claude-sonnet-4-5-20250929'), [cheaperLowerRank, preferredButExpensive]);
|
|
72
|
+
expect(selected?.peerId).toBe(preferredButExpensive.peerId);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects peers when output price exceeds buyer max even if input is within max', () => {
|
|
76
|
+
const router = new LocalProxyRouter({
|
|
77
|
+
maxPricing: {
|
|
78
|
+
defaults: { inputUsdPerMillion: 50, outputUsdPerMillion: 10 },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const overpricedOutputPeer = makePeer({
|
|
83
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
84
|
+
providerPricing: {
|
|
85
|
+
anthropic: {
|
|
86
|
+
defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 20 },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
defaultInputUsdPerMillion: 5,
|
|
90
|
+
defaultOutputUsdPerMillion: 20,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(router.selectPeer(makeRequest('claude-sonnet-4-5-20250929'), [overpricedOutputPeer])).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('uses model-specific seller offer pricing when request model is present', () => {
|
|
97
|
+
const router = new LocalProxyRouter({
|
|
98
|
+
maxPricing: {
|
|
99
|
+
defaults: { inputUsdPerMillion: 1_000, outputUsdPerMillion: 1_000 },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const peerA = makePeer({
|
|
104
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
105
|
+
providerPricing: {
|
|
106
|
+
anthropic: {
|
|
107
|
+
defaults: { inputUsdPerMillion: 10, outputUsdPerMillion: 10 },
|
|
108
|
+
models: {
|
|
109
|
+
'model-a': { inputUsdPerMillion: 90, outputUsdPerMillion: 90 },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
defaultInputUsdPerMillion: 10,
|
|
114
|
+
defaultOutputUsdPerMillion: 10,
|
|
115
|
+
});
|
|
116
|
+
const peerB = makePeer({
|
|
117
|
+
peerId: '2'.repeat(64) as PeerInfo['peerId'],
|
|
118
|
+
providerPricing: {
|
|
119
|
+
anthropic: {
|
|
120
|
+
defaults: { inputUsdPerMillion: 20, outputUsdPerMillion: 20 },
|
|
121
|
+
models: {
|
|
122
|
+
'model-a': { inputUsdPerMillion: 5, outputUsdPerMillion: 5 },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
defaultInputUsdPerMillion: 20,
|
|
127
|
+
defaultOutputUsdPerMillion: 20,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const selected = router.selectPeer(makeRequest('model-a'), [peerA, peerB]);
|
|
131
|
+
expect(selected?.peerId).toBe(peerB.peerId);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('falls back to provider defaults when request model is absent', () => {
|
|
135
|
+
const router = new LocalProxyRouter({
|
|
136
|
+
maxPricing: {
|
|
137
|
+
defaults: { inputUsdPerMillion: 1_000, outputUsdPerMillion: 1_000 },
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const expensiveDefault = makePeer({
|
|
142
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
143
|
+
providerPricing: {
|
|
144
|
+
anthropic: {
|
|
145
|
+
defaults: { inputUsdPerMillion: 40, outputUsdPerMillion: 40 },
|
|
146
|
+
models: {
|
|
147
|
+
'model-a': { inputUsdPerMillion: 1, outputUsdPerMillion: 1 },
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
defaultInputUsdPerMillion: 40,
|
|
152
|
+
defaultOutputUsdPerMillion: 40,
|
|
153
|
+
});
|
|
154
|
+
const cheapDefault = makePeer({
|
|
155
|
+
peerId: '2'.repeat(64) as PeerInfo['peerId'],
|
|
156
|
+
providerPricing: {
|
|
157
|
+
anthropic: {
|
|
158
|
+
defaults: { inputUsdPerMillion: 5, outputUsdPerMillion: 5 },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
defaultInputUsdPerMillion: 5,
|
|
162
|
+
defaultOutputUsdPerMillion: 5,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const selected = router.selectPeer(makeRequest(undefined), [expensiveDefault, cheapDefault]);
|
|
166
|
+
expect(selected?.peerId).toBe(cheapDefault.peerId);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('puts peers on cooldown after failure threshold and re-allows them later', () => {
|
|
170
|
+
let now = 1_000_000;
|
|
171
|
+
const router = new LocalProxyRouter({
|
|
172
|
+
maxFailures: 2,
|
|
173
|
+
failureCooldownMs: 500,
|
|
174
|
+
now: () => now,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const flaky = makePeer({ peerId: '1'.repeat(64) as PeerInfo['peerId'], lastSeen: now });
|
|
178
|
+
const fallback = makePeer({ peerId: 'f'.repeat(64) as PeerInfo['peerId'], lastSeen: now });
|
|
179
|
+
|
|
180
|
+
router.onResult(flaky, { success: false, latencyMs: 300, tokens: 0 });
|
|
181
|
+
router.onResult(flaky, { success: false, latencyMs: 300, tokens: 0 });
|
|
182
|
+
|
|
183
|
+
// Flaky is cooling down; fallback should be selected.
|
|
184
|
+
expect(router.selectPeer(makeRequest(), [flaky, fallback])?.peerId).toBe(fallback.peerId);
|
|
185
|
+
|
|
186
|
+
now += 501;
|
|
187
|
+
// Cooldown expired; flaky is allowed again, but still penalized by reliability history.
|
|
188
|
+
expect(router.selectPeer(makeRequest(), [flaky, fallback])?.peerId).toBe(fallback.peerId);
|
|
189
|
+
// It should still be selectable when no alternatives exist.
|
|
190
|
+
expect(router.selectPeer(makeRequest(), [flaky])?.peerId).toBe(flaky.peerId);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('filters out peers below minimum reputation', () => {
|
|
194
|
+
const router = new LocalProxyRouter({
|
|
195
|
+
minReputation: 70,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const lowRep = makePeer({
|
|
199
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
200
|
+
reputationScore: 40,
|
|
201
|
+
trustScore: 40,
|
|
202
|
+
});
|
|
203
|
+
const highRep = makePeer({
|
|
204
|
+
peerId: '2'.repeat(64) as PeerInfo['peerId'],
|
|
205
|
+
reputationScore: 90,
|
|
206
|
+
trustScore: 90,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const selected = router.selectPeer(makeRequest(), [lowRep, highRep]);
|
|
210
|
+
expect(selected?.peerId).toBe(highRep.peerId);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('keeps peers eligible when reputation fields are missing', () => {
|
|
214
|
+
const router = new LocalProxyRouter();
|
|
215
|
+
const unrated = makePeer({
|
|
216
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
217
|
+
reputationScore: undefined,
|
|
218
|
+
trustScore: undefined,
|
|
219
|
+
onChainReputation: undefined,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const selected = router.selectPeer(makeRequest(), [unrated]);
|
|
223
|
+
expect(selected?.peerId).toBe(unrated.peerId);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('treats on-chain zero reputation with zero sessions as unrated', () => {
|
|
227
|
+
const router = new LocalProxyRouter();
|
|
228
|
+
const newSeller = makePeer({
|
|
229
|
+
peerId: '3'.repeat(64) as PeerInfo['peerId'],
|
|
230
|
+
trustScore: 0,
|
|
231
|
+
reputationScore: undefined,
|
|
232
|
+
onChainReputation: 0,
|
|
233
|
+
onChainSessionCount: 0,
|
|
234
|
+
onChainDisputeCount: 0,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const selected = router.selectPeer(makeRequest(), [newSeller]);
|
|
238
|
+
expect(selected?.peerId).toBe(newSeller.peerId);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('ignores empty provider entries when selecting a peer provider', () => {
|
|
242
|
+
const router = new LocalProxyRouter();
|
|
243
|
+
const malformedProviders = makePeer({
|
|
244
|
+
peerId: '1'.repeat(64) as PeerInfo['peerId'],
|
|
245
|
+
providers: ['', 'anthropic'],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const selected = router.selectPeer(makeRequest(), [malformedProviders]);
|
|
249
|
+
expect(selected?.peerId).toBe(malformedProviders.peerId);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('returns null when no peers are available', () => {
|
|
253
|
+
const router = new LocalProxyRouter();
|
|
254
|
+
expect(router.selectPeer(makeRequest(), [])).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
});
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { Router, PeerInfo, SerializedHttpRequest } from '@antseed/node';
|
|
2
|
+
import {
|
|
3
|
+
scoreCandidates,
|
|
4
|
+
PeerMetricsTracker,
|
|
5
|
+
type TokenPricingUsdPerMillion,
|
|
6
|
+
type ScoringWeights,
|
|
7
|
+
} from '@antseed/router-core';
|
|
8
|
+
|
|
9
|
+
export interface BuyerMaxPricingConfig {
|
|
10
|
+
defaults: TokenPricingUsdPerMillion;
|
|
11
|
+
providers?: Record<string, {
|
|
12
|
+
defaults?: TokenPricingUsdPerMillion;
|
|
13
|
+
models?: Record<string, TokenPricingUsdPerMillion>;
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LocalProxyRouterConfig {
|
|
18
|
+
preferredProviders?: string[];
|
|
19
|
+
minReputation?: number;
|
|
20
|
+
maxPricing?: BuyerMaxPricingConfig;
|
|
21
|
+
maxFailures?: number;
|
|
22
|
+
failureCooldownMs?: number;
|
|
23
|
+
maxPeerStalenessMs?: number;
|
|
24
|
+
weights?: Partial<ScoringWeights>;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class LocalProxyRouter implements Router {
|
|
29
|
+
private readonly _preferredProviders: string[];
|
|
30
|
+
private readonly _minReputation: number;
|
|
31
|
+
private readonly _maxPricing: BuyerMaxPricingConfig;
|
|
32
|
+
private readonly _maxFailures: number;
|
|
33
|
+
private readonly _maxPeerStalenessMs: number;
|
|
34
|
+
private readonly _now: () => number;
|
|
35
|
+
private readonly _weights: Partial<ScoringWeights> | undefined;
|
|
36
|
+
private readonly _metrics: PeerMetricsTracker;
|
|
37
|
+
|
|
38
|
+
constructor(config?: LocalProxyRouterConfig) {
|
|
39
|
+
this._preferredProviders = (config?.preferredProviders ?? [])
|
|
40
|
+
.map((provider) => provider.trim())
|
|
41
|
+
.filter((provider) => provider.length > 0);
|
|
42
|
+
this._minReputation = config?.minReputation ?? 50;
|
|
43
|
+
this._maxPricing = {
|
|
44
|
+
defaults: {
|
|
45
|
+
inputUsdPerMillion: config?.maxPricing?.defaults.inputUsdPerMillion ?? Number.POSITIVE_INFINITY,
|
|
46
|
+
outputUsdPerMillion: config?.maxPricing?.defaults.outputUsdPerMillion ?? Number.POSITIVE_INFINITY,
|
|
47
|
+
},
|
|
48
|
+
...(config?.maxPricing?.providers ? { providers: config.maxPricing.providers } : {}),
|
|
49
|
+
};
|
|
50
|
+
this._maxFailures = Math.max(1, config?.maxFailures ?? 3);
|
|
51
|
+
this._maxPeerStalenessMs = Math.max(1, config?.maxPeerStalenessMs ?? 300_000);
|
|
52
|
+
this._now = config?.now ?? (() => Date.now());
|
|
53
|
+
this._weights = config?.weights;
|
|
54
|
+
this._metrics = new PeerMetricsTracker({
|
|
55
|
+
maxFailures: this._maxFailures,
|
|
56
|
+
failureCooldownMs: Math.max(1, config?.failureCooldownMs ?? 30_000),
|
|
57
|
+
now: this._now,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
selectPeer(req: SerializedHttpRequest, peers: PeerInfo[]): PeerInfo | null {
|
|
62
|
+
const now = this._now();
|
|
63
|
+
const requestedModel = this._extractRequestedModel(req);
|
|
64
|
+
|
|
65
|
+
const candidates: {
|
|
66
|
+
peer: PeerInfo;
|
|
67
|
+
provider: string;
|
|
68
|
+
providerRank: number;
|
|
69
|
+
offer: TokenPricingUsdPerMillion;
|
|
70
|
+
}[] = [];
|
|
71
|
+
|
|
72
|
+
for (const peer of peers) {
|
|
73
|
+
// Reputation filter
|
|
74
|
+
if (this._hasReputation(peer)) {
|
|
75
|
+
const reputation = this._effectiveReputation(peer);
|
|
76
|
+
if (reputation < this._minReputation) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cooldown filter
|
|
82
|
+
if (this._metrics.isCoolingDown(peer.peerId)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Provider availability filter
|
|
87
|
+
const selectedProvider = this._selectProviderForPeer(peer);
|
|
88
|
+
if (!selectedProvider) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Pricing filter
|
|
93
|
+
const offer = this._resolvePeerOfferPrice(peer, selectedProvider.provider, requestedModel);
|
|
94
|
+
if (!offer) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const max = this._resolveBuyerMaxPrice(selectedProvider.provider, requestedModel);
|
|
99
|
+
if (offer.inputUsdPerMillion > max.inputUsdPerMillion || offer.outputUsdPerMillion > max.outputUsdPerMillion) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
candidates.push({
|
|
104
|
+
peer,
|
|
105
|
+
provider: selectedProvider.provider,
|
|
106
|
+
providerRank: selectedProvider.rank,
|
|
107
|
+
offer,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (candidates.length === 0) return null;
|
|
112
|
+
|
|
113
|
+
// Provider preference filtering
|
|
114
|
+
let providerFiltered = candidates;
|
|
115
|
+
if (this._preferredProviders.length > 0) {
|
|
116
|
+
const bestRank = Math.min(...candidates.map((c) => c.providerRank));
|
|
117
|
+
providerFiltered = candidates.filter((c) => c.providerRank === bestRank);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (providerFiltered.length === 1) {
|
|
121
|
+
return providerFiltered[0]!.peer;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Delegate scoring to router-core
|
|
125
|
+
const scoringInput = providerFiltered.map((c) => ({
|
|
126
|
+
peer: c.peer,
|
|
127
|
+
provider: c.provider,
|
|
128
|
+
providerRank: c.providerRank,
|
|
129
|
+
offer: c.offer,
|
|
130
|
+
metrics: this._metrics.getMetrics(c.peer.peerId),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
const scored = scoreCandidates(scoringInput, {
|
|
134
|
+
now,
|
|
135
|
+
medianLatency: this._metrics.getMedianLatency(),
|
|
136
|
+
maxPeerStalenessMs: this._maxPeerStalenessMs,
|
|
137
|
+
maxFailures: this._maxFailures,
|
|
138
|
+
weights: this._weights,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return scored[0]?.peer ?? null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onResult(
|
|
145
|
+
peer: PeerInfo,
|
|
146
|
+
result: { success: boolean; latencyMs: number; tokens: number },
|
|
147
|
+
): void {
|
|
148
|
+
this._metrics.recordResult(peer.peerId, {
|
|
149
|
+
success: result.success,
|
|
150
|
+
latencyMs: result.latencyMs,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private _effectiveReputation(p: PeerInfo): number {
|
|
155
|
+
if (p.onChainReputation !== undefined) {
|
|
156
|
+
return p.onChainReputation;
|
|
157
|
+
}
|
|
158
|
+
return p.trustScore ?? p.reputationScore ?? 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private _hasReputation(p: PeerInfo): boolean {
|
|
162
|
+
if (this._isFiniteNonNegative(p.onChainReputation)) {
|
|
163
|
+
const sessionCount = this._isFiniteNonNegative(p.onChainSessionCount) ? p.onChainSessionCount : undefined;
|
|
164
|
+
const disputeCount = this._isFiniteNonNegative(p.onChainDisputeCount) ? p.onChainDisputeCount : undefined;
|
|
165
|
+
if (sessionCount !== undefined || disputeCount !== undefined) {
|
|
166
|
+
return (sessionCount ?? 0) > 0 || (disputeCount ?? 0) > 0;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return this._isFiniteNonNegative(p.trustScore) || this._isFiniteNonNegative(p.reputationScore);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private _extractRequestedModel(req: SerializedHttpRequest): string | null {
|
|
175
|
+
const contentType = req.headers['content-type'] ?? req.headers['Content-Type'] ?? '';
|
|
176
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(new TextDecoder().decode(req.body)) as unknown;
|
|
182
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const model = (parsed as Record<string, unknown>)['model'];
|
|
186
|
+
return typeof model === 'string' && model.trim().length > 0 ? model.trim() : null;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private _selectProviderForPeer(peer: PeerInfo): { provider: string; rank: number } | null {
|
|
193
|
+
const availableProviders = peer.providers
|
|
194
|
+
.map((provider) => provider.trim())
|
|
195
|
+
.filter((provider) => provider.length > 0);
|
|
196
|
+
|
|
197
|
+
if (this._preferredProviders.length === 0) {
|
|
198
|
+
const provider = availableProviders[0];
|
|
199
|
+
return provider ? { provider, rank: Number.MAX_SAFE_INTEGER } : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < this._preferredProviders.length; i++) {
|
|
203
|
+
const preferred = this._preferredProviders[i]!;
|
|
204
|
+
if (availableProviders.includes(preferred)) {
|
|
205
|
+
return { provider: preferred, rank: i };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private _resolvePeerOfferPrice(
|
|
213
|
+
peer: PeerInfo,
|
|
214
|
+
provider: string,
|
|
215
|
+
model: string | null,
|
|
216
|
+
): TokenPricingUsdPerMillion | null {
|
|
217
|
+
const providerPricing = peer.providerPricing?.[provider];
|
|
218
|
+
|
|
219
|
+
if (model) {
|
|
220
|
+
const modelSpecific = providerPricing?.models?.[model];
|
|
221
|
+
if (modelSpecific && this._isValidOffer(modelSpecific)) {
|
|
222
|
+
return modelSpecific;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const providerDefaults = providerPricing?.defaults;
|
|
227
|
+
if (providerDefaults && this._isValidOffer(providerDefaults)) {
|
|
228
|
+
return providerDefaults;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
this._isFiniteNonNegative(peer.defaultInputUsdPerMillion) &&
|
|
233
|
+
this._isFiniteNonNegative(peer.defaultOutputUsdPerMillion)
|
|
234
|
+
) {
|
|
235
|
+
return {
|
|
236
|
+
inputUsdPerMillion: peer.defaultInputUsdPerMillion,
|
|
237
|
+
outputUsdPerMillion: peer.defaultOutputUsdPerMillion,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private _resolveBuyerMaxPrice(provider: string, model: string | null): TokenPricingUsdPerMillion {
|
|
245
|
+
const providerPricing = this._maxPricing.providers?.[provider];
|
|
246
|
+
|
|
247
|
+
if (model) {
|
|
248
|
+
const modelOverride = providerPricing?.models?.[model];
|
|
249
|
+
if (modelOverride && this._isValidOffer(modelOverride)) {
|
|
250
|
+
return modelOverride;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const providerDefaults = providerPricing?.defaults;
|
|
255
|
+
if (providerDefaults && this._isValidOffer(providerDefaults)) {
|
|
256
|
+
return providerDefaults;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return this._maxPricing.defaults;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private _isFiniteNonNegative(value: number | undefined): value is number {
|
|
263
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private _isValidOffer(offer: TokenPricingUsdPerMillion): boolean {
|
|
267
|
+
return (
|
|
268
|
+
this._isFiniteNonNegative(offer.inputUsdPerMillion) &&
|
|
269
|
+
this._isFiniteNonNegative(offer.outputUsdPerMillion)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|