pi-freerouter 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 +79 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.js +47 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +68 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +12 -0
- package/dist/router.js +53 -0
- package/dist/router.js.map +1 -0
- package/dist/router.test.d.ts +1 -0
- package/dist/router.test.js +74 -0
- package/dist/router.test.js.map +1 -0
- package/dist/stream.d.ts +11 -0
- package/dist/stream.js +292 -0
- package/dist/stream.js.map +1 -0
- package/dist/stream.test.d.ts +1 -0
- package/dist/stream.test.js +129 -0
- package/dist/stream.test.js.map +1 -0
- package/dist/types.d.ts +186 -0
- package/dist/types.js +88 -0
- package/dist/types.js.map +1 -0
- package/package.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# pi-freerouter
|
|
2
|
+
|
|
3
|
+
Pi coding agent extension that routes every request through OpenRouter's free model tier — no paid API key needed beyond your OpenRouter account.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
**1. Install**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:pi-freerouter
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**2. Set your OpenRouter API key**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
export OPENROUTER_API_KEY=sk-or-...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Free key at [openrouter.ai/keys](https://openrouter.ai/keys). No credit card required.
|
|
20
|
+
|
|
21
|
+
**3. Start Pi**
|
|
22
|
+
|
|
23
|
+
FreeRouter appears in the model picker and is set as the default automatically.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## How free model routing works
|
|
28
|
+
|
|
29
|
+
OpenRouter exposes dozens of free models (models with a `:free` suffix). Each has its own rate limit — typically a few requests per minute per model. The trick is to spread load across all of them automatically.
|
|
30
|
+
|
|
31
|
+
### Parallel racing
|
|
32
|
+
|
|
33
|
+
Every time Pi sends a request, pi-freerouter doesn't pick one model and hope for the best. It picks the **next 3 available models** from its sorted list and fires all three requests simultaneously.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Request arrives
|
|
37
|
+
│
|
|
38
|
+
├── model A ──────────────────── first token ──▶ WINNER → stream to Pi
|
|
39
|
+
├── model B ───── (slower) → aborted
|
|
40
|
+
└── model C ─── (even slower) → aborted
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Whichever model emits its first token wins. The other two are immediately cancelled. Pi sees a single clean stream — it has no idea a race happened.
|
|
44
|
+
|
|
45
|
+
### Automatic fallback
|
|
46
|
+
|
|
47
|
+
When a model hits its rate limit (HTTP 429) or fails (5xx), it's marked as exhausted and skipped for 90 seconds. The next batch of 3 models takes over. Timed-out models (no response in 30 seconds) recover faster — after 15 seconds.
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Batch 1: [model A, model B, model C] → all hit quota
|
|
51
|
+
Batch 2: [model D, model E, model F] → model D wins
|
|
52
|
+
↑ model A–C recover after 90s and rejoin the pool
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Provider priority
|
|
56
|
+
|
|
57
|
+
Free models are sorted so the lowest-latency inference providers are always tried first:
|
|
58
|
+
|
|
59
|
+
1. Groq
|
|
60
|
+
2. Cerebras
|
|
61
|
+
3. Fireworks
|
|
62
|
+
4. Together
|
|
63
|
+
5. Mistral
|
|
64
|
+
6. Everything else (sorted by context window ascending)
|
|
65
|
+
|
|
66
|
+
### Model list refresh
|
|
67
|
+
|
|
68
|
+
The list of available free models is fetched at startup and refreshed every hour in the background, so long-running Pi sessions automatically pick up newly added models.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Requirements
|
|
73
|
+
|
|
74
|
+
- [Pi coding agent](https://pi.dev) v0.78+
|
|
75
|
+
- OpenRouter API key (free tier is sufficient)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
2
|
+
const DEFAULT_MAX_TOKENS = 4_096;
|
|
3
|
+
// Providers known for low latency on free tier, ordered by preference
|
|
4
|
+
const FAST_PROVIDER_PREFIXES = [
|
|
5
|
+
"groq/",
|
|
6
|
+
"cerebras/",
|
|
7
|
+
"fireworks/",
|
|
8
|
+
"together/",
|
|
9
|
+
"mistralai/",
|
|
10
|
+
];
|
|
11
|
+
function speedScore(modelId) {
|
|
12
|
+
const lower = modelId.toLowerCase();
|
|
13
|
+
const idx = FAST_PROVIDER_PREFIXES.findIndex((prefix) => lower.startsWith(prefix));
|
|
14
|
+
return idx === -1 ? FAST_PROVIDER_PREFIXES.length : idx;
|
|
15
|
+
}
|
|
16
|
+
export async function fetchFreeModels(apiKey) {
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
throw new Error("OPENROUTER_API_KEY is required");
|
|
19
|
+
}
|
|
20
|
+
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
|
21
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`Failed to fetch OpenRouter models: ${response.status} ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
const payload = (await response.json());
|
|
27
|
+
const models = (payload.data ?? []).filter((m) => m.id.includes(":free"));
|
|
28
|
+
// Sort: fast providers first, then by context size ascending (smaller = faster inference)
|
|
29
|
+
models.sort((a, b) => {
|
|
30
|
+
const scoreDiff = speedScore(a.id) - speedScore(b.id);
|
|
31
|
+
if (scoreDiff !== 0)
|
|
32
|
+
return scoreDiff;
|
|
33
|
+
const aCtx = a.context_length ?? DEFAULT_CONTEXT_WINDOW;
|
|
34
|
+
const bCtx = b.context_length ?? DEFAULT_CONTEXT_WINDOW;
|
|
35
|
+
return aCtx - bCtx;
|
|
36
|
+
});
|
|
37
|
+
return models.map((m) => ({
|
|
38
|
+
id: m.id,
|
|
39
|
+
name: m.name,
|
|
40
|
+
reasoning: false,
|
|
41
|
+
input: ["text"],
|
|
42
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
43
|
+
contextWindow: m.context_length ?? DEFAULT_CONTEXT_WINDOW,
|
|
44
|
+
maxTokens: m.top_provider?.max_completion_tokens ?? DEFAULT_MAX_TOKENS,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAEA,MAAM,sBAAsB,GAAG,OAAO,CAAC;AACvC,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAEjC,sEAAsE;AACtE,MAAM,sBAAsB,GAAG;IAC7B,OAAO;IACP,WAAW;IACX,YAAY;IACZ,WAAW;IACX,YAAY;CACb,CAAC;AAEF,SAAS,UAAU,CAAC,OAAe;IACjC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,GAAG,GAAG,sBAAsB,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IACnF,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;AAC1D,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAc;IAClD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,qCAAqC,EAAE;QAClE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE;KAC/C,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,sCAAsC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IAClG,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAC;IAEpE,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAE1E,0FAA0F;IAC1F,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACnB,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACtD,IAAI,SAAS,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACtC,MAAM,IAAI,GAAG,CAAC,CAAC,cAAc,IAAI,sBAAsB,CAAC;QACxD,MAAM,IAAI,GAAG,CAAC,CAAC,cAAc,IAAI,sBAAsB,CAAC;QACxD,OAAO,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxB,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,CAAC,MAAM,CAAyB;QACvC,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;QAC1D,aAAa,EAAE,CAAC,CAAC,cAAc,IAAI,sBAAsB;QACzD,SAAS,EAAE,CAAC,CAAC,YAAY,EAAE,qBAAqB,IAAI,kBAAkB;KACvE,CAAC,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
3
|
+
describe("fetchFreeModels", () => {
|
|
4
|
+
let originalFetch;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
originalFetch = globalThis.fetch;
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
globalThis.fetch = originalFetch;
|
|
10
|
+
});
|
|
11
|
+
it("filters only :free models and maps fields", async () => {
|
|
12
|
+
const mockPayload = {
|
|
13
|
+
data: [
|
|
14
|
+
{
|
|
15
|
+
id: "meta-llama/llama-3.2-3b:free",
|
|
16
|
+
name: "Llama 3.2 3B (free)",
|
|
17
|
+
context_length: 131072,
|
|
18
|
+
top_provider: { max_completion_tokens: 8192 },
|
|
19
|
+
pricing: { prompt: "0", completion: "0" },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "openai/gpt-4o",
|
|
23
|
+
name: "GPT-4o",
|
|
24
|
+
context_length: 128000,
|
|
25
|
+
pricing: { prompt: "0.000005", completion: "0.000015" },
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
globalThis.fetch = async () => ({
|
|
30
|
+
ok: true,
|
|
31
|
+
json: async () => mockPayload,
|
|
32
|
+
});
|
|
33
|
+
const { fetchFreeModels } = await import("./discovery.js");
|
|
34
|
+
const models = await fetchFreeModels("sk-or-test");
|
|
35
|
+
assert.equal(models.length, 1);
|
|
36
|
+
assert.equal(models[0].id, "meta-llama/llama-3.2-3b:free");
|
|
37
|
+
assert.equal(models[0].contextWindow, 131072);
|
|
38
|
+
assert.equal(models[0].maxTokens, 8192);
|
|
39
|
+
assert.deepEqual(models[0].cost, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
40
|
+
});
|
|
41
|
+
it("throws if apiKey is empty", async () => {
|
|
42
|
+
const { fetchFreeModels } = await import("./discovery.js");
|
|
43
|
+
await assert.rejects(() => fetchFreeModels(""), /OPENROUTER_API_KEY/);
|
|
44
|
+
});
|
|
45
|
+
it("throws if fetch response is not ok", async () => {
|
|
46
|
+
globalThis.fetch = async () => ({ ok: false, status: 401, statusText: "Unauthorized" });
|
|
47
|
+
const { fetchFreeModels } = await import("./discovery.js");
|
|
48
|
+
await assert.rejects(() => fetchFreeModels("sk-or-bad"), /401/);
|
|
49
|
+
});
|
|
50
|
+
it("sorts fast providers before unknown providers", async () => {
|
|
51
|
+
const mockPayload = {
|
|
52
|
+
data: [
|
|
53
|
+
{ id: "meta-llama/llama-3.1-8b:free", name: "Llama (unknown provider)", context_length: 32768 },
|
|
54
|
+
{ id: "groq/llama-3.3-70b:free", name: "Groq Llama", context_length: 131072 },
|
|
55
|
+
{ id: "cerebras/llama3.1-8b:free", name: "Cerebras Llama", context_length: 8192 },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
globalThis.fetch = async () => ({ ok: true, json: async () => mockPayload });
|
|
59
|
+
const { fetchFreeModels } = await import("./discovery.js");
|
|
60
|
+
const models = await fetchFreeModels("sk-or-test");
|
|
61
|
+
assert.equal(models.length, 3);
|
|
62
|
+
// Groq (score 0) before Cerebras (score 1) before unknown (score 5)
|
|
63
|
+
assert.ok(models[0].id.startsWith("groq/"), `expected groq first, got ${models[0].id}`);
|
|
64
|
+
assert.ok(models[1].id.startsWith("cerebras/"), `expected cerebras second, got ${models[1].id}`);
|
|
65
|
+
assert.ok(models[2].id.startsWith("meta-llama/"), `expected meta-llama last, got ${models[2].id}`);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
//# sourceMappingURL=discovery.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.test.js","sourceRoot":"","sources":["../src/discovery.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createAssistantMessageEventStream } from "./types.js";
|
|
2
|
+
import { fetchFreeModels } from "./discovery.js";
|
|
3
|
+
import { FreeRouter } from "./router.js";
|
|
4
|
+
import { streamFreeModel, ModelExhaustedError, ModelFatalError } from "./stream.js";
|
|
5
|
+
// Race this many free models simultaneously; first to stream wins.
|
|
6
|
+
const RACE_WIDTH = 3;
|
|
7
|
+
// If no candidate emits its first token within this window, abort and try the
|
|
8
|
+
// next batch. Free models routinely take 10-30 s to first token; 30 s ensures
|
|
9
|
+
// we don't thrash through the pool on legitimately slow (but live) models.
|
|
10
|
+
const FIRST_TOKEN_TIMEOUT_MS = 30_000;
|
|
11
|
+
// Re-fetch the free model list every hour so long-running Pi sessions pick up
|
|
12
|
+
// newly available models (and stop wasting retries on removed ones).
|
|
13
|
+
const REFRESH_INTERVAL_MS = 60 * 60 * 1_000;
|
|
14
|
+
function mergeSignals(...signals) {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
for (const sig of signals) {
|
|
17
|
+
if (!sig)
|
|
18
|
+
continue;
|
|
19
|
+
if (sig.aborted) {
|
|
20
|
+
controller.abort();
|
|
21
|
+
return controller.signal;
|
|
22
|
+
}
|
|
23
|
+
sig.addEventListener("abort", () => controller.abort(), { once: true });
|
|
24
|
+
}
|
|
25
|
+
return controller.signal;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start candidateIds concurrently and forward events from the first model that
|
|
29
|
+
* emits `text_start` (or a valid empty `done`) to outStream; abort the rest.
|
|
30
|
+
*
|
|
31
|
+
* Correctness notes:
|
|
32
|
+
* - Uses Map<candidateIdx, Promise> so delete(idx) is always by candidate index,
|
|
33
|
+
* never by array position (which diverges after the first re-push).
|
|
34
|
+
* - Snapshots exhaustedIds at each return so late floating-.catch() pushes after
|
|
35
|
+
* raceModels returns don't corrupt the caller's view.
|
|
36
|
+
* - Tracks fatalError (e.g. 402) separately from quota exhaustion.
|
|
37
|
+
*/
|
|
38
|
+
async function raceModels(candidateIds, context, apiKey, outStream, parentSignal, maxTokens) {
|
|
39
|
+
const controllers = candidateIds.map(() => new AbortController());
|
|
40
|
+
const proxyStreams = candidateIds.map(() => createAssistantMessageEventStream());
|
|
41
|
+
const exhaustedIds = [];
|
|
42
|
+
let fatalError;
|
|
43
|
+
// Start all candidates concurrently; each writes to its own isolated proxy stream.
|
|
44
|
+
candidateIds.forEach((modelId, i) => {
|
|
45
|
+
const sig = parentSignal
|
|
46
|
+
? mergeSignals(parentSignal, controllers[i].signal)
|
|
47
|
+
: controllers[i].signal;
|
|
48
|
+
streamFreeModel(modelId, context, apiKey, proxyStreams[i], sig, maxTokens).catch((err) => {
|
|
49
|
+
if (err instanceof ModelExhaustedError) {
|
|
50
|
+
exhaustedIds.push(modelId);
|
|
51
|
+
}
|
|
52
|
+
else if (err instanceof ModelFatalError) {
|
|
53
|
+
fatalError = fatalError ?? err; // keep first fatal error
|
|
54
|
+
}
|
|
55
|
+
proxyStreams[i].end(); // ensure iterator terminates
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
const iterators = proxyStreams.map((s) => s[Symbol.asyncIterator]());
|
|
59
|
+
const buffers = candidateIds.map(() => []);
|
|
60
|
+
const nextFrom = (idx) => iterators[idx].next().then((result) => ({ idx, result }));
|
|
61
|
+
// Map<candidateIdx, Promise> so delete(idx) is always correct regardless of
|
|
62
|
+
// insertion order — the old array-based filter((_,j)=>j!==idx) was wrong after
|
|
63
|
+
// any re-push because j (array position) diverges from idx (candidate index).
|
|
64
|
+
const pending = new Map(candidateIds.map((_, i) => [i, nextFrom(i)]));
|
|
65
|
+
let timeoutHandle;
|
|
66
|
+
const deadline = new Promise((resolve) => {
|
|
67
|
+
timeoutHandle = setTimeout(() => resolve({ __timeout: true }), FIRST_TOKEN_TIMEOUT_MS);
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
while (pending.size > 0) {
|
|
71
|
+
const resolved = await Promise.race([
|
|
72
|
+
...pending.values(),
|
|
73
|
+
deadline,
|
|
74
|
+
]);
|
|
75
|
+
if ("__timeout" in resolved) {
|
|
76
|
+
controllers.forEach((c) => c.abort());
|
|
77
|
+
return { winner: null, exhaustedIds: [...exhaustedIds], timedOut: true, fatalError };
|
|
78
|
+
}
|
|
79
|
+
const { idx, result } = resolved;
|
|
80
|
+
pending.delete(idx); // safe: keyed by candidate idx, not array position
|
|
81
|
+
if (result.done)
|
|
82
|
+
continue; // this candidate's stream ended; move on
|
|
83
|
+
const event = result.value;
|
|
84
|
+
buffers[idx].push(event);
|
|
85
|
+
// text_start → normal text streaming win
|
|
86
|
+
// toolcall_start → model is making a tool call; also a valid win
|
|
87
|
+
// done → valid but empty response (no text/tool content); still a winner
|
|
88
|
+
if (event.type === "text_start" || event.type === "toolcall_start" || event.type === "done") {
|
|
89
|
+
controllers.forEach((c, j) => { if (j !== idx)
|
|
90
|
+
c.abort(); });
|
|
91
|
+
for (const e of buffers[idx])
|
|
92
|
+
outStream.push(e);
|
|
93
|
+
if (event.type === "done") {
|
|
94
|
+
outStream.end();
|
|
95
|
+
return { winner: candidateIds[idx], exhaustedIds: [...exhaustedIds], timedOut: false };
|
|
96
|
+
}
|
|
97
|
+
// text_start / toolcall_start: pipe remaining events from the winner to outStream.
|
|
98
|
+
for await (const e of { [Symbol.asyncIterator]: () => iterators[idx] }) {
|
|
99
|
+
outStream.push(e);
|
|
100
|
+
if (e.type === "done" || e.type === "error") {
|
|
101
|
+
outStream.end();
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
outStream.end(); // defensive: no-op if already ended via done/error above
|
|
106
|
+
return { winner: candidateIds[idx], exhaustedIds: [...exhaustedIds], timedOut: false };
|
|
107
|
+
}
|
|
108
|
+
if (event.type === "error") {
|
|
109
|
+
// Non-quota failure; stream ends next tick — don't re-add to pending.
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Any other event (e.g., thinking_start before text_start): keep consuming.
|
|
113
|
+
pending.set(idx, nextFrom(idx));
|
|
114
|
+
}
|
|
115
|
+
return { winner: null, exhaustedIds: [...exhaustedIds], timedOut: false, fatalError };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
clearTimeout(timeoutHandle);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const BASE_ERROR_OUTPUT = {
|
|
122
|
+
role: "assistant",
|
|
123
|
+
api: "openrouter",
|
|
124
|
+
provider: "freerouter",
|
|
125
|
+
model: "free-router",
|
|
126
|
+
usage: {
|
|
127
|
+
input: 0,
|
|
128
|
+
output: 0,
|
|
129
|
+
cacheRead: 0,
|
|
130
|
+
cacheWrite: 0,
|
|
131
|
+
totalTokens: 0,
|
|
132
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
133
|
+
},
|
|
134
|
+
stopReason: "error",
|
|
135
|
+
};
|
|
136
|
+
export default async function (pi) {
|
|
137
|
+
const apiKeyRaw = process.env.OPENROUTER_API_KEY;
|
|
138
|
+
if (!apiKeyRaw) {
|
|
139
|
+
throw new Error("pi-freerouter: OPENROUTER_API_KEY is not set");
|
|
140
|
+
}
|
|
141
|
+
// Explicit string type so closures (streamSimple, refreshModels) see `string`,
|
|
142
|
+
// not `string | undefined` — TypeScript doesn't carry narrowing into inner fns.
|
|
143
|
+
const apiKey = apiKeyRaw;
|
|
144
|
+
const freeModels = await fetchFreeModels(apiKey);
|
|
145
|
+
if (freeModels.length === 0) {
|
|
146
|
+
throw new Error("pi-freerouter: No free models found on OpenRouter");
|
|
147
|
+
}
|
|
148
|
+
// `let` so the background refresh can replace it; each streamSimple call
|
|
149
|
+
// captures its own snapshot via `const localRouter = router`.
|
|
150
|
+
let router = new FreeRouter(freeModels.map((m) => m.id));
|
|
151
|
+
const maxContext = Math.max(...freeModels.map((m) => m.contextWindow));
|
|
152
|
+
const maxTokens = Math.max(...freeModels.map((m) => m.maxTokens));
|
|
153
|
+
// Hourly background refresh — picks up new free models without restarting Pi.
|
|
154
|
+
async function refreshModels() {
|
|
155
|
+
try {
|
|
156
|
+
const fresh = await fetchFreeModels(apiKey);
|
|
157
|
+
if (fresh.length > 0) {
|
|
158
|
+
router = new FreeRouter(fresh.map((m) => m.id));
|
|
159
|
+
console.log(`[pi-freerouter] Model list refreshed: ${fresh.length} free models`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.warn("[pi-freerouter] Failed to refresh free model list:", err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const refreshTimer = setInterval(() => { void refreshModels(); }, REFRESH_INTERVAL_MS);
|
|
167
|
+
// Don't keep the Node process alive solely for this timer.
|
|
168
|
+
if (typeof refreshTimer.unref === "function") {
|
|
169
|
+
refreshTimer.unref();
|
|
170
|
+
}
|
|
171
|
+
pi.registerProvider("freerouter", {
|
|
172
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
173
|
+
apiKey,
|
|
174
|
+
api: "openai-completions",
|
|
175
|
+
models: [
|
|
176
|
+
{
|
|
177
|
+
id: "free-router",
|
|
178
|
+
name: "FreeRouter",
|
|
179
|
+
reasoning: false,
|
|
180
|
+
input: ["text"],
|
|
181
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
182
|
+
contextWindow: maxContext,
|
|
183
|
+
maxTokens,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
+
streamSimple: ((_model, context, options) => {
|
|
188
|
+
const stream = createAssistantMessageEventStream();
|
|
189
|
+
let streamClosed = false;
|
|
190
|
+
// Capture router at request start so mid-request refreshes don't affect
|
|
191
|
+
// this in-flight request's exhaustion tracking.
|
|
192
|
+
const localRouter = router;
|
|
193
|
+
(async () => {
|
|
194
|
+
while (true) {
|
|
195
|
+
// Check abort before starting each batch.
|
|
196
|
+
if (options?.signal?.aborted) {
|
|
197
|
+
const abortMsg = "Request was cancelled.";
|
|
198
|
+
streamClosed = true;
|
|
199
|
+
stream.push({
|
|
200
|
+
type: "error",
|
|
201
|
+
reason: "aborted",
|
|
202
|
+
error: {
|
|
203
|
+
...BASE_ERROR_OUTPUT,
|
|
204
|
+
content: [],
|
|
205
|
+
errorMessage: abortMsg,
|
|
206
|
+
timestamp: Date.now(),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
stream.end();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const candidates = localRouter.nextModels(RACE_WIDTH);
|
|
213
|
+
if (candidates.length === 0)
|
|
214
|
+
break;
|
|
215
|
+
console.log(`[pi-freerouter] Racing: ${candidates.join(", ")}`);
|
|
216
|
+
const { winner, exhaustedIds, timedOut, fatalError } = await raceModels(candidates, context, apiKey, stream, options?.signal, options?.maxTokens);
|
|
217
|
+
// Quota-exceeded models: long TTL (90s).
|
|
218
|
+
exhaustedIds.forEach((id) => localRouter.markExhausted(id));
|
|
219
|
+
if (winner !== null) {
|
|
220
|
+
console.log(`[pi-freerouter] Winner: ${winner}`);
|
|
221
|
+
streamClosed = true;
|
|
222
|
+
return; // raceModels wrote all events and called stream.end()
|
|
223
|
+
}
|
|
224
|
+
// Fatal error (e.g., 402 Insufficient Credits) — surface immediately.
|
|
225
|
+
if (fatalError) {
|
|
226
|
+
const errMsg = fatalError.message;
|
|
227
|
+
streamClosed = true;
|
|
228
|
+
stream.push({
|
|
229
|
+
type: "error",
|
|
230
|
+
reason: "error",
|
|
231
|
+
error: {
|
|
232
|
+
...BASE_ERROR_OUTPUT,
|
|
233
|
+
content: [{ type: "text", text: errMsg }],
|
|
234
|
+
errorMessage: errMsg,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
stream.end();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// Check abort — race may have ended because parent signal fired.
|
|
242
|
+
if (options?.signal?.aborted) {
|
|
243
|
+
const abortMsg = "Request was cancelled.";
|
|
244
|
+
streamClosed = true;
|
|
245
|
+
stream.push({
|
|
246
|
+
type: "error",
|
|
247
|
+
reason: "aborted",
|
|
248
|
+
error: {
|
|
249
|
+
...BASE_ERROR_OUTPUT,
|
|
250
|
+
content: [],
|
|
251
|
+
errorMessage: abortMsg,
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
stream.end();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// No winner — skip candidates we haven't already marked exhausted.
|
|
259
|
+
// Timeout → short TTL (15s): model is alive but slow, recover quickly.
|
|
260
|
+
// Other failure → long TTL (90s): treat as quota/error, avoid for longer.
|
|
261
|
+
candidates.forEach((id) => {
|
|
262
|
+
if (!exhaustedIds.includes(id)) {
|
|
263
|
+
if (timedOut) {
|
|
264
|
+
localRouter.markSlow(id);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
localRouter.markExhausted(id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// All models currently in TTL cooldown.
|
|
273
|
+
const errMsg = "All free models exhausted. They will recover automatically — please try again in a moment.";
|
|
274
|
+
streamClosed = true;
|
|
275
|
+
stream.push({
|
|
276
|
+
type: "error",
|
|
277
|
+
reason: "error",
|
|
278
|
+
error: {
|
|
279
|
+
...BASE_ERROR_OUTPUT,
|
|
280
|
+
content: [{ type: "text", text: errMsg }],
|
|
281
|
+
errorMessage: errMsg,
|
|
282
|
+
timestamp: Date.now(),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
stream.end();
|
|
286
|
+
})().catch((err) => {
|
|
287
|
+
if (!streamClosed) {
|
|
288
|
+
const errMsg = String(err);
|
|
289
|
+
stream.push({
|
|
290
|
+
type: "error",
|
|
291
|
+
reason: "error",
|
|
292
|
+
error: {
|
|
293
|
+
...BASE_ERROR_OUTPUT,
|
|
294
|
+
content: [],
|
|
295
|
+
errorMessage: errMsg,
|
|
296
|
+
timestamp: Date.now(),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
stream.end();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// Cast: local stream satisfies the pi-ai class interface at runtime;
|
|
303
|
+
// private fields on the pi-ai class prevent structural assignability.
|
|
304
|
+
return stream;
|
|
305
|
+
}),
|
|
306
|
+
});
|
|
307
|
+
// Auto-activate FreeRouter as the default model on session start.
|
|
308
|
+
pi.on("session_start", async (_event, handlerCtx) => {
|
|
309
|
+
try {
|
|
310
|
+
const registry = handlerCtx?.modelRegistry;
|
|
311
|
+
const freeRouterModel = registry?.find?.("freerouter", "free-router");
|
|
312
|
+
if (freeRouterModel) {
|
|
313
|
+
await pi.setModel(freeRouterModel);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
console.warn("[pi-freerouter] Failed to set FreeRouter as active model:", err);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
//# 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,12 @@
|
|
|
1
|
+
export declare class FreeRouter {
|
|
2
|
+
private readonly models;
|
|
3
|
+
private readonly exhaustionTtlMs;
|
|
4
|
+
private readonly exhausted;
|
|
5
|
+
constructor(models: readonly string[], exhaustionTtlMs?: number);
|
|
6
|
+
nextModel(): string | null;
|
|
7
|
+
nextModels(count: number): string[];
|
|
8
|
+
/** Mark model as quota-exhausted (429/5xx). Long TTL — don't retry soon. */
|
|
9
|
+
markExhausted(id: string): void;
|
|
10
|
+
/** Mark model as slow (first-token timeout). Short TTL — try others first, recover fast. */
|
|
11
|
+
markSlow(id: string): void;
|
|
12
|
+
}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// 90 s for quota exhaustion (429/5xx) — matches OpenRouter's per-minute reset window.
|
|
2
|
+
const DEFAULT_EXHAUSTION_TTL_MS = 90_000;
|
|
3
|
+
// 15 s for a first-token timeout — model is alive but slow; back-off briefly so
|
|
4
|
+
// the next batch tries different candidates without burning the whole pool.
|
|
5
|
+
const SLOW_TTL_MS = 15_000;
|
|
6
|
+
export class FreeRouter {
|
|
7
|
+
models;
|
|
8
|
+
exhaustionTtlMs;
|
|
9
|
+
// modelId → { at: timestamp, ttl: ms }
|
|
10
|
+
exhausted = new Map();
|
|
11
|
+
constructor(models, exhaustionTtlMs = DEFAULT_EXHAUSTION_TTL_MS) {
|
|
12
|
+
this.models = models;
|
|
13
|
+
this.exhaustionTtlMs = exhaustionTtlMs;
|
|
14
|
+
}
|
|
15
|
+
nextModel() {
|
|
16
|
+
return this.nextModels(1)[0] ?? null;
|
|
17
|
+
}
|
|
18
|
+
nextModels(count) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const result = [];
|
|
21
|
+
for (const id of this.models) {
|
|
22
|
+
if (result.length >= count)
|
|
23
|
+
break;
|
|
24
|
+
const entry = this.exhausted.get(id);
|
|
25
|
+
if (entry !== undefined) {
|
|
26
|
+
if (now - entry.at < entry.ttl)
|
|
27
|
+
continue;
|
|
28
|
+
this.exhausted.delete(id); // TTL expired — back in rotation
|
|
29
|
+
}
|
|
30
|
+
result.push(id);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/** Mark model as quota-exhausted (429/5xx). Long TTL — don't retry soon. */
|
|
35
|
+
markExhausted(id) {
|
|
36
|
+
if (!this.models.includes(id)) {
|
|
37
|
+
console.warn(`[pi-freerouter] markExhausted called with unknown model ID: ${id}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
this.exhausted.set(id, { at: Date.now(), ttl: this.exhaustionTtlMs });
|
|
41
|
+
}
|
|
42
|
+
/** Mark model as slow (first-token timeout). Short TTL — try others first, recover fast. */
|
|
43
|
+
markSlow(id) {
|
|
44
|
+
if (!this.models.includes(id))
|
|
45
|
+
return;
|
|
46
|
+
// Don't downgrade an already-exhausted model to the shorter slow TTL.
|
|
47
|
+
const existing = this.exhausted.get(id);
|
|
48
|
+
if (existing && existing.ttl >= this.exhaustionTtlMs)
|
|
49
|
+
return;
|
|
50
|
+
this.exhausted.set(id, { at: Date.now(), ttl: SLOW_TTL_MS });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,MAAM,yBAAyB,GAAG,MAAM,CAAC;AACzC,gFAAgF;AAChF,4EAA4E;AAC5E,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,MAAM,OAAO,UAAU;IAKF;IACA;IALnB,uCAAuC;IACtB,SAAS,GAAG,IAAI,GAAG,EAAuC,CAAC;IAE5E,YACmB,MAAyB,EACzB,kBAAkB,yBAAyB;QAD3C,WAAM,GAAN,MAAM,CAAmB;QACzB,oBAAe,GAAf,eAAe,CAA4B;IAC3D,CAAC;IAEJ,SAAS;QACP,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACvC,CAAC;IAED,UAAU,CAAC,KAAa;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAa,EAAE,CAAC;QAE5B,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7B,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK;gBAAE,MAAM;YAClC,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;gBACxB,IAAI,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,CAAC,GAAG;oBAAE,SAAS;gBACzC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,iCAAiC;YAC9D,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClB,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,4EAA4E;IAC5E,aAAa,CAAC,EAAU;QACtB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,+DAA+D,EAAE,EAAE,CAAC,CAAC;YAClF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,4FAA4F;IAC5F,QAAQ,CAAC,EAAU;QACjB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,OAAO;QACtC,sEAAsE;QACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACxC,IAAI,QAAQ,IAAI,QAAQ,CAAC,GAAG,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO;QAC7D,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC;IAC/D,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|