codex-dev-mcp-suite 1.1.0 → 1.2.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/.env.example +3 -0
- package/CHANGELOG.md +12 -0
- package/bin/_meta.mjs +69 -0
- package/bin/checkpoint.mjs +8 -1
- package/bin/context-pack.mjs +8 -1
- package/bin/devjournal.mjs +8 -1
- package/bin/project-memory.mjs +8 -1
- package/devjournal/provider-chain.js +63 -10
- package/devjournal/rerank.js +29 -13
- package/docs/configuration.md +14 -0
- package/package.json +2 -1
- package/project-memory/provider-chain.js +63 -10
- package/project-memory/rerank.js +29 -13
package/.env.example
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
# MCP_PROVIDER_CHAIN3_API_KEY=<openrouter-key>
|
|
23
23
|
# MCP_PROVIDER_CHAIN3_MODEL=openai/gpt-4o-mini
|
|
24
24
|
|
|
25
|
+
# Cooldown (ms) before retrying a provider after a 429/5xx/timeout failure.
|
|
26
|
+
# MCP_PROVIDER_COOLDOWN_MS=60000
|
|
27
|
+
|
|
25
28
|
# Base URL of an OpenAI-compatible API (must expose /v1/...)
|
|
26
29
|
MCP_LLM_BASE_URL=http://localhost:11434/v1
|
|
27
30
|
# API key for that endpoint (leave blank for local servers that don't need one)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0 - 2026-06-15
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- CLI meta handling for all bins: `--help`, `--version`, and `--doctor` (config diagnostics with API keys redacted) before the stdio server starts.
|
|
7
|
+
- Provider diagnostics `providerChainDiagnostics()` that lists active providers with redacted keys and flags incomplete numbered slots.
|
|
8
|
+
- Per-provider cooldown on `429`/`5xx`/timeout/network failures via `MCP_PROVIDER_COOLDOWN_MS` (default `60000`).
|
|
9
|
+
- CI job that packs the tarball, installs it in a temp dir, and runs each bin `--version`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Rerank now skips providers that are cooling down and records success/failure outcomes.
|
|
13
|
+
- Rerank never logs API keys or response bodies.
|
|
14
|
+
|
|
3
15
|
## 1.1.0 - 2026-06-15
|
|
4
16
|
|
|
5
17
|
### Added
|
package/bin/_meta.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
function pkgVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
|
|
10
|
+
return JSON.parse(raw).version || "unknown";
|
|
11
|
+
} catch { return "unknown"; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function redactKey(name) {
|
|
15
|
+
const v = String(process.env[name] || "");
|
|
16
|
+
return v ? `set (${v.length} chars)` : "not set";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function doctorLines(meta) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
lines.push(`${meta.title} doctor`);
|
|
22
|
+
lines.push(`version: ${pkgVersion()}`);
|
|
23
|
+
lines.push(`node: ${process.version}`);
|
|
24
|
+
for (const [label, name] of meta.storage || []) {
|
|
25
|
+
lines.push(`${label}: ${process.env[name] || meta.storageDefaults?.[name] || "(default)"}`);
|
|
26
|
+
}
|
|
27
|
+
const det = /^(1|true|yes|on)$/i.test(String(process.env.MCP_DETERMINISTIC_FALLBACK || "").trim());
|
|
28
|
+
lines.push(`deterministic no-network: ${det ? "on" : "off"}`);
|
|
29
|
+
if (meta.usesModel) {
|
|
30
|
+
lines.push("model/provider config (keys redacted):");
|
|
31
|
+
lines.push(` MCP_LLM_BASE_URL: ${process.env.MCP_LLM_BASE_URL || "(unset)"}`);
|
|
32
|
+
lines.push(` MCP_LLM_API_KEY: ${redactKey("MCP_LLM_API_KEY")}`);
|
|
33
|
+
lines.push(` MCP_PROVIDER_PRIMARY: ${process.env.MCP_PROVIDER_PRIMARY || "(unset)"}`);
|
|
34
|
+
lines.push(` MCP_PROVIDER_PRIMARY_API_KEY: ${redactKey("MCP_PROVIDER_PRIMARY_API_KEY")}`);
|
|
35
|
+
lines.push(` MCP_EMBED_API_KEY: ${redactKey("MCP_EMBED_API_KEY")}`);
|
|
36
|
+
lines.push(` MCP_RERANK_API_KEY: ${redactKey("MCP_RERANK_API_KEY")}`);
|
|
37
|
+
} else {
|
|
38
|
+
lines.push("model/provider config: not used by this server");
|
|
39
|
+
}
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function handleCliMeta(meta) {
|
|
44
|
+
const argv = process.argv.slice(2);
|
|
45
|
+
if (argv.includes("-v") || argv.includes("--version")) {
|
|
46
|
+
process.stdout.write(`${pkgVersion()}\n`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
50
|
+
const out = [
|
|
51
|
+
`${meta.title} (${meta.bin}) v${pkgVersion()}`,
|
|
52
|
+
"",
|
|
53
|
+
"An MCP server that speaks JSON-RPC over stdio. Launch it from an MCP client.",
|
|
54
|
+
"",
|
|
55
|
+
"Usage:",
|
|
56
|
+
` ${meta.bin} start the MCP stdio server`,
|
|
57
|
+
` ${meta.bin} --version print version`,
|
|
58
|
+
` ${meta.bin} --doctor print config diagnostics (API keys redacted)`,
|
|
59
|
+
` ${meta.bin} --help show this help`,
|
|
60
|
+
];
|
|
61
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
if (argv.includes("--doctor")) {
|
|
65
|
+
process.stdout.write(doctorLines(meta).join("\n") + "\n");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
package/bin/checkpoint.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { handleCliMeta } from "./_meta.mjs";
|
|
3
|
+
handleCliMeta({
|
|
4
|
+
bin: "checkpoint-mcp",
|
|
5
|
+
title: "Checkpoint MCP",
|
|
6
|
+
usesModel: false,
|
|
7
|
+
storage: [["store", "CHECKPOINT_DIR"]],
|
|
8
|
+
});
|
|
9
|
+
import("../checkpoint/server.js");
|
package/bin/context-pack.mjs
CHANGED
package/bin/devjournal.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { handleCliMeta } from "./_meta.mjs";
|
|
3
|
+
handleCliMeta({
|
|
4
|
+
bin: "devjournal-mcp",
|
|
5
|
+
title: "Dev Journal MCP",
|
|
6
|
+
usesModel: true,
|
|
7
|
+
storage: [["store", "JOURNAL_DIR"]],
|
|
8
|
+
});
|
|
9
|
+
import("../devjournal/server.js");
|
package/bin/project-memory.mjs
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "
|
|
2
|
+
import { handleCliMeta } from "./_meta.mjs";
|
|
3
|
+
handleCliMeta({
|
|
4
|
+
bin: "project-memory-mcp",
|
|
5
|
+
title: "Project Memory MCP",
|
|
6
|
+
usesModel: true,
|
|
7
|
+
storage: [["vault", "MEMORY_VAULT_DIR"]],
|
|
8
|
+
});
|
|
9
|
+
import("../project-memory/server.js");
|
|
@@ -7,6 +7,11 @@ function value(name) {
|
|
|
7
7
|
return String(process.env[name] || "").trim();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function redactKey(key) {
|
|
11
|
+
const k = String(key || "");
|
|
12
|
+
return k ? `set (${k.length} chars)` : "not set";
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
function providerEnv(prefix) {
|
|
11
16
|
const label = value(prefix);
|
|
12
17
|
const base = value(`${prefix}_BASE_URL`);
|
|
@@ -16,20 +21,34 @@ function providerEnv(prefix) {
|
|
|
16
21
|
return { label, base, key, model };
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
function slotIssues(prefix) {
|
|
25
|
+
const present = ["", "_BASE_URL", "_API_KEY", "_MODEL"].some((s) => value(`${prefix}${s}`));
|
|
26
|
+
if (!present) return null;
|
|
27
|
+
const missing = [];
|
|
28
|
+
if (!value(prefix)) missing.push(`${prefix}`);
|
|
29
|
+
if (!value(`${prefix}_BASE_URL`)) missing.push(`${prefix}_BASE_URL`);
|
|
30
|
+
if (!value(`${prefix}_API_KEY`)) missing.push(`${prefix}_API_KEY`);
|
|
31
|
+
if (!value(`${prefix}_MODEL`)) missing.push(`${prefix}_MODEL`);
|
|
32
|
+
return missing.length ? { prefix, missing } : null;
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
function numberedPrefixes() {
|
|
36
|
+
const prefixes = ["MCP_PROVIDER_PRIMARY"];
|
|
37
|
+
const nums = Object.keys(process.env)
|
|
38
|
+
.map((name) => name.match(/^MCP_PROVIDER_CHAIN(\d+)/)?.[1])
|
|
26
39
|
.filter(Boolean)
|
|
27
40
|
.map(Number)
|
|
28
|
-
.filter((n) => n >= 2)
|
|
29
|
-
|
|
41
|
+
.filter((n) => n >= 2);
|
|
42
|
+
for (const n of [...new Set(nums)].sort((a, b) => a - b)) {
|
|
43
|
+
prefixes.push(`MCP_PROVIDER_CHAIN${n}`);
|
|
44
|
+
}
|
|
45
|
+
return prefixes;
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
function numberedProviders() {
|
|
49
|
+
const providers = [];
|
|
50
|
+
for (const prefix of numberedPrefixes()) {
|
|
51
|
+
const provider = providerEnv(prefix);
|
|
33
52
|
if (provider) providers.push(provider);
|
|
34
53
|
}
|
|
35
54
|
return providers;
|
|
@@ -54,3 +73,37 @@ export function providerChainConfig() {
|
|
|
54
73
|
}
|
|
55
74
|
return { providers, enabled: providers.length > 0, deterministic };
|
|
56
75
|
}
|
|
76
|
+
|
|
77
|
+
export function providerChainDiagnostics() {
|
|
78
|
+
const cfg = providerChainConfig();
|
|
79
|
+
const issues = [];
|
|
80
|
+
for (const prefix of numberedPrefixes()) {
|
|
81
|
+
const issue = slotIssues(prefix);
|
|
82
|
+
if (issue) issues.push(issue);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
deterministic: cfg.deterministic,
|
|
86
|
+
enabled: cfg.enabled,
|
|
87
|
+
providers: cfg.providers.map((p) => ({ label: p.label, base: p.base, model: p.model, apiKey: redactKey(p.key) })),
|
|
88
|
+
issues,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const COOLDOWN_MS = Number(process.env.MCP_PROVIDER_COOLDOWN_MS || 60000);
|
|
93
|
+
const cooldowns = new Map();
|
|
94
|
+
|
|
95
|
+
export function isCoolingDown(key, now = Date.now()) {
|
|
96
|
+
const until = cooldowns.get(key);
|
|
97
|
+
if (!until) return false;
|
|
98
|
+
if (now >= until) { cooldowns.delete(key); return false; }
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function recordOutcome(key, ok, now = Date.now()) {
|
|
103
|
+
if (ok) { cooldowns.delete(key); return; }
|
|
104
|
+
cooldowns.set(key, now + COOLDOWN_MS);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function _resetCooldowns() {
|
|
108
|
+
cooldowns.clear();
|
|
109
|
+
}
|
package/devjournal/rerank.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LLM reranker via any OpenAI-compatible /v1/chat/completions model.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Tries provider chain slots in order, skipping any currently cooling down
|
|
4
|
+
* after a recent failure. Degrades gracefully: returns null on any failure so
|
|
5
|
+
* callers fall back to keyword ordering. Never throws. Never logs keys/bodies.
|
|
6
6
|
*/
|
|
7
7
|
import http from "http";
|
|
8
8
|
import https from "https";
|
|
9
9
|
import { URL } from "url";
|
|
10
10
|
import { deterministicEnabled } from "./env.js";
|
|
11
|
-
import { providerChainConfig } from "./provider-chain.js";
|
|
11
|
+
import { providerChainConfig, isCoolingDown, recordOutcome } from "./provider-chain.js";
|
|
12
12
|
|
|
13
13
|
const TIMEOUT_MS = Number(process.env.RERANK_TIMEOUT_MS || 30000);
|
|
14
14
|
const ENABLED = process.env.RERANK_ENABLED !== "0";
|
|
@@ -25,10 +25,19 @@ export function rerankConfig() {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function providerKey(provider) {
|
|
29
|
+
return `${provider.label}|${provider.base}|${provider.model}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves { content, retryable }. retryable=true means try the next provider
|
|
34
|
+
* (timeout, network error, 429, or 5xx). retryable=false with null content
|
|
35
|
+
* means a definitive non-retryable response (e.g. 4xx auth).
|
|
36
|
+
*/
|
|
28
37
|
function chatWithProvider(provider, messages) {
|
|
29
38
|
return new Promise((resolve) => {
|
|
30
39
|
let u;
|
|
31
|
-
try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve(null); }
|
|
40
|
+
try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve({ content: null, retryable: false }); }
|
|
32
41
|
const payload = Buffer.from(JSON.stringify({ model: provider.model, stream: false, temperature: 0, max_tokens: 200, messages }));
|
|
33
42
|
const lib = u.protocol === "https:" ? https : http;
|
|
34
43
|
const req = lib.request(
|
|
@@ -44,10 +53,14 @@ function chatWithProvider(provider, messages) {
|
|
|
44
53
|
let body = "";
|
|
45
54
|
res.on("data", (c) => (body += c));
|
|
46
55
|
res.on("end", () => {
|
|
47
|
-
|
|
56
|
+
const status = res.statusCode || 0;
|
|
57
|
+
if (status !== 200) {
|
|
58
|
+
const retryable = status === 429 || status >= 500;
|
|
59
|
+
return resolve({ content: null, retryable });
|
|
60
|
+
}
|
|
48
61
|
try {
|
|
49
62
|
const j = JSON.parse(body);
|
|
50
|
-
return resolve(j?.choices?.[0]?.message?.content ?? null);
|
|
63
|
+
return resolve({ content: j?.choices?.[0]?.message?.content ?? null, retryable: false });
|
|
51
64
|
} catch { /* maybe SSE */ }
|
|
52
65
|
try {
|
|
53
66
|
let acc = "";
|
|
@@ -59,13 +72,13 @@ function chatWithProvider(provider, messages) {
|
|
|
59
72
|
const j = JSON.parse(d);
|
|
60
73
|
acc += j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
|
|
61
74
|
}
|
|
62
|
-
return resolve(acc || null);
|
|
63
|
-
} catch { return resolve(null); }
|
|
75
|
+
return resolve({ content: acc || null, retryable: false });
|
|
76
|
+
} catch { return resolve({ content: null, retryable: true }); }
|
|
64
77
|
});
|
|
65
78
|
}
|
|
66
79
|
);
|
|
67
|
-
req.on("error", () => resolve(null));
|
|
68
|
-
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
80
|
+
req.on("error", () => resolve({ content: null, retryable: true }));
|
|
81
|
+
req.on("timeout", () => { req.destroy(); resolve({ content: null, retryable: true }); });
|
|
69
82
|
req.write(payload);
|
|
70
83
|
req.end();
|
|
71
84
|
});
|
|
@@ -75,8 +88,11 @@ async function chat(messages) {
|
|
|
75
88
|
if (!ENABLED || deterministicEnabled()) return null;
|
|
76
89
|
const { providers } = providerChainConfig();
|
|
77
90
|
for (const provider of providers) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
91
|
+
const key = providerKey(provider);
|
|
92
|
+
if (isCoolingDown(key)) continue;
|
|
93
|
+
const { content, retryable } = await chatWithProvider(provider, messages);
|
|
94
|
+
if (content) { recordOutcome(key, true); return content; }
|
|
95
|
+
if (retryable) recordOutcome(key, false);
|
|
80
96
|
}
|
|
81
97
|
return null;
|
|
82
98
|
}
|
package/docs/configuration.md
CHANGED
|
@@ -73,6 +73,20 @@ MCP_PROVIDER_CHAIN3_MODEL=openai/gpt-4o-mini
|
|
|
73
73
|
|
|
74
74
|
You can add `MCP_PROVIDER_CHAIN4_*`, `MCP_PROVIDER_CHAIN5_*`, and so on. The provider name is only a label; any OpenAI-compatible endpoint works when `BASE_URL`, `API_KEY`, and `MODEL` are set.
|
|
75
75
|
|
|
76
|
+
On a `429`, `5xx`, timeout, or network error, a provider is put on a short cooldown and the next slot is tried. Tune the window with `MCP_PROVIDER_COOLDOWN_MS` (default `60000`).
|
|
77
|
+
|
|
78
|
+
## Diagnostics
|
|
79
|
+
|
|
80
|
+
Each server bin supports inspection flags that never start the server:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
project-memory-mcp --version
|
|
84
|
+
project-memory-mcp --doctor # storage + model/provider config, API keys redacted
|
|
85
|
+
project-memory-mcp --help
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`--doctor` reports configured providers and storage paths and shows API keys only as `set (<n> chars)` or `not set`, never the raw value.
|
|
89
|
+
|
|
76
90
|
Example OpenRouter-style config:
|
|
77
91
|
|
|
78
92
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-dev-mcp-suite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Four local, file-based MCP servers for solo devs/vibecoders: persistent project memory, session handoff/resume, git-independent file checkpoints, and token-efficient project briefings. Works with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"bin/*.mjs",
|
|
45
|
+
"bin/_meta.mjs",
|
|
45
46
|
"project-memory/*.js",
|
|
46
47
|
"project-memory/server.js",
|
|
47
48
|
"project-memory/embedding.js",
|
|
@@ -7,6 +7,11 @@ function value(name) {
|
|
|
7
7
|
return String(process.env[name] || "").trim();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function redactKey(key) {
|
|
11
|
+
const k = String(key || "");
|
|
12
|
+
return k ? `set (${k.length} chars)` : "not set";
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
function providerEnv(prefix) {
|
|
11
16
|
const label = value(prefix);
|
|
12
17
|
const base = value(`${prefix}_BASE_URL`);
|
|
@@ -16,20 +21,34 @@ function providerEnv(prefix) {
|
|
|
16
21
|
return { label, base, key, model };
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
function slotIssues(prefix) {
|
|
25
|
+
const present = ["", "_BASE_URL", "_API_KEY", "_MODEL"].some((s) => value(`${prefix}${s}`));
|
|
26
|
+
if (!present) return null;
|
|
27
|
+
const missing = [];
|
|
28
|
+
if (!value(prefix)) missing.push(`${prefix}`);
|
|
29
|
+
if (!value(`${prefix}_BASE_URL`)) missing.push(`${prefix}_BASE_URL`);
|
|
30
|
+
if (!value(`${prefix}_API_KEY`)) missing.push(`${prefix}_API_KEY`);
|
|
31
|
+
if (!value(`${prefix}_MODEL`)) missing.push(`${prefix}_MODEL`);
|
|
32
|
+
return missing.length ? { prefix, missing } : null;
|
|
33
|
+
}
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
function numberedPrefixes() {
|
|
36
|
+
const prefixes = ["MCP_PROVIDER_PRIMARY"];
|
|
37
|
+
const nums = Object.keys(process.env)
|
|
38
|
+
.map((name) => name.match(/^MCP_PROVIDER_CHAIN(\d+)/)?.[1])
|
|
26
39
|
.filter(Boolean)
|
|
27
40
|
.map(Number)
|
|
28
|
-
.filter((n) => n >= 2)
|
|
29
|
-
|
|
41
|
+
.filter((n) => n >= 2);
|
|
42
|
+
for (const n of [...new Set(nums)].sort((a, b) => a - b)) {
|
|
43
|
+
prefixes.push(`MCP_PROVIDER_CHAIN${n}`);
|
|
44
|
+
}
|
|
45
|
+
return prefixes;
|
|
46
|
+
}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
function numberedProviders() {
|
|
49
|
+
const providers = [];
|
|
50
|
+
for (const prefix of numberedPrefixes()) {
|
|
51
|
+
const provider = providerEnv(prefix);
|
|
33
52
|
if (provider) providers.push(provider);
|
|
34
53
|
}
|
|
35
54
|
return providers;
|
|
@@ -54,3 +73,37 @@ export function providerChainConfig() {
|
|
|
54
73
|
}
|
|
55
74
|
return { providers, enabled: providers.length > 0, deterministic };
|
|
56
75
|
}
|
|
76
|
+
|
|
77
|
+
export function providerChainDiagnostics() {
|
|
78
|
+
const cfg = providerChainConfig();
|
|
79
|
+
const issues = [];
|
|
80
|
+
for (const prefix of numberedPrefixes()) {
|
|
81
|
+
const issue = slotIssues(prefix);
|
|
82
|
+
if (issue) issues.push(issue);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
deterministic: cfg.deterministic,
|
|
86
|
+
enabled: cfg.enabled,
|
|
87
|
+
providers: cfg.providers.map((p) => ({ label: p.label, base: p.base, model: p.model, apiKey: redactKey(p.key) })),
|
|
88
|
+
issues,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const COOLDOWN_MS = Number(process.env.MCP_PROVIDER_COOLDOWN_MS || 60000);
|
|
93
|
+
const cooldowns = new Map();
|
|
94
|
+
|
|
95
|
+
export function isCoolingDown(key, now = Date.now()) {
|
|
96
|
+
const until = cooldowns.get(key);
|
|
97
|
+
if (!until) return false;
|
|
98
|
+
if (now >= until) { cooldowns.delete(key); return false; }
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function recordOutcome(key, ok, now = Date.now()) {
|
|
103
|
+
if (ok) { cooldowns.delete(key); return; }
|
|
104
|
+
cooldowns.set(key, now + COOLDOWN_MS);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function _resetCooldowns() {
|
|
108
|
+
cooldowns.clear();
|
|
109
|
+
}
|
package/project-memory/rerank.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LLM reranker via any OpenAI-compatible /v1/chat/completions model.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Tries provider chain slots in order, skipping any currently cooling down
|
|
4
|
+
* after a recent failure. Degrades gracefully: returns null on any failure so
|
|
5
|
+
* callers fall back to keyword ordering. Never throws. Never logs keys/bodies.
|
|
6
6
|
*/
|
|
7
7
|
import http from "http";
|
|
8
8
|
import https from "https";
|
|
9
9
|
import { URL } from "url";
|
|
10
10
|
import { deterministicEnabled } from "./env.js";
|
|
11
|
-
import { providerChainConfig } from "./provider-chain.js";
|
|
11
|
+
import { providerChainConfig, isCoolingDown, recordOutcome } from "./provider-chain.js";
|
|
12
12
|
|
|
13
13
|
const TIMEOUT_MS = Number(process.env.RERANK_TIMEOUT_MS || 30000);
|
|
14
14
|
const ENABLED = process.env.RERANK_ENABLED !== "0";
|
|
@@ -25,10 +25,19 @@ export function rerankConfig() {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function providerKey(provider) {
|
|
29
|
+
return `${provider.label}|${provider.base}|${provider.model}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolves { content, retryable }. retryable=true means try the next provider
|
|
34
|
+
* (timeout, network error, 429, or 5xx). retryable=false with null content
|
|
35
|
+
* means a definitive non-retryable response (e.g. 4xx auth).
|
|
36
|
+
*/
|
|
28
37
|
function chatWithProvider(provider, messages) {
|
|
29
38
|
return new Promise((resolve) => {
|
|
30
39
|
let u;
|
|
31
|
-
try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve(null); }
|
|
40
|
+
try { u = new URL("/v1/chat/completions", provider.base); } catch { return resolve({ content: null, retryable: false }); }
|
|
32
41
|
const payload = Buffer.from(JSON.stringify({ model: provider.model, stream: false, temperature: 0, max_tokens: 200, messages }));
|
|
33
42
|
const lib = u.protocol === "https:" ? https : http;
|
|
34
43
|
const req = lib.request(
|
|
@@ -44,10 +53,14 @@ function chatWithProvider(provider, messages) {
|
|
|
44
53
|
let body = "";
|
|
45
54
|
res.on("data", (c) => (body += c));
|
|
46
55
|
res.on("end", () => {
|
|
47
|
-
|
|
56
|
+
const status = res.statusCode || 0;
|
|
57
|
+
if (status !== 200) {
|
|
58
|
+
const retryable = status === 429 || status >= 500;
|
|
59
|
+
return resolve({ content: null, retryable });
|
|
60
|
+
}
|
|
48
61
|
try {
|
|
49
62
|
const j = JSON.parse(body);
|
|
50
|
-
return resolve(j?.choices?.[0]?.message?.content ?? null);
|
|
63
|
+
return resolve({ content: j?.choices?.[0]?.message?.content ?? null, retryable: false });
|
|
51
64
|
} catch { /* maybe SSE */ }
|
|
52
65
|
try {
|
|
53
66
|
let acc = "";
|
|
@@ -59,13 +72,13 @@ function chatWithProvider(provider, messages) {
|
|
|
59
72
|
const j = JSON.parse(d);
|
|
60
73
|
acc += j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
|
|
61
74
|
}
|
|
62
|
-
return resolve(acc || null);
|
|
63
|
-
} catch { return resolve(null); }
|
|
75
|
+
return resolve({ content: acc || null, retryable: false });
|
|
76
|
+
} catch { return resolve({ content: null, retryable: true }); }
|
|
64
77
|
});
|
|
65
78
|
}
|
|
66
79
|
);
|
|
67
|
-
req.on("error", () => resolve(null));
|
|
68
|
-
req.on("timeout", () => { req.destroy(); resolve(null); });
|
|
80
|
+
req.on("error", () => resolve({ content: null, retryable: true }));
|
|
81
|
+
req.on("timeout", () => { req.destroy(); resolve({ content: null, retryable: true }); });
|
|
69
82
|
req.write(payload);
|
|
70
83
|
req.end();
|
|
71
84
|
});
|
|
@@ -75,8 +88,11 @@ async function chat(messages) {
|
|
|
75
88
|
if (!ENABLED || deterministicEnabled()) return null;
|
|
76
89
|
const { providers } = providerChainConfig();
|
|
77
90
|
for (const provider of providers) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
91
|
+
const key = providerKey(provider);
|
|
92
|
+
if (isCoolingDown(key)) continue;
|
|
93
|
+
const { content, retryable } = await chatWithProvider(provider, messages);
|
|
94
|
+
if (content) { recordOutcome(key, true); return content; }
|
|
95
|
+
if (retryable) recordOutcome(key, false);
|
|
80
96
|
}
|
|
81
97
|
return null;
|
|
82
98
|
}
|