@jrwoodcock/modelmux 2.0.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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +373 -0
- package/package.json +46 -0
- package/src/server.js +686 -0
- package/src/test.js +207 -0
package/src/test.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file test.js
|
|
4
|
+
* @description modelmux — connectivity and file I/O smoke test
|
|
5
|
+
*
|
|
6
|
+
* Verifies that each configured API key is valid and reachable,
|
|
7
|
+
* and that the Node.js process can read and write temporary files.
|
|
8
|
+
* Run this after installation or any time you want to confirm
|
|
9
|
+
* modelmux is ready to use.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node ~/.modelmux/src/test.js
|
|
13
|
+
*
|
|
14
|
+
* Each check reports one of three outcomes:
|
|
15
|
+
* PASS — the check succeeded
|
|
16
|
+
* SKIP — the relevant API key is not set; the check was not attempted
|
|
17
|
+
* FAIL — the check ran but produced an unexpected result or error
|
|
18
|
+
*
|
|
19
|
+
* @author Jason R. Woodcock
|
|
20
|
+
* @version 2.0.0
|
|
21
|
+
* @license Apache-2.0 — see the LICENSE and NOTICE files.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
25
|
+
|
|
26
|
+
// A minimal prompt that should produce a short, deterministic response.
|
|
27
|
+
// This is used for all three API connectivity checks.
|
|
28
|
+
const PING_PROMPT = "Reply with exactly: OK";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// API connectivity checks
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Checks that the Anthropic API key is valid by sending a minimal message.
|
|
36
|
+
*
|
|
37
|
+
* @returns {Promise<string>} A PASS, SKIP, or FAIL result string.
|
|
38
|
+
*/
|
|
39
|
+
async function testClaude() {
|
|
40
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
41
|
+
if (!key) return "SKIP (ANTHROPIC_API_KEY not set)";
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"x-api-key": key,
|
|
49
|
+
"anthropic-version": "2023-06-01",
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
|
|
53
|
+
max_tokens: 16,
|
|
54
|
+
messages: [{ role: "user", content: PING_PROMPT }],
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return `FAIL (HTTP ${response.status}: ${await response.text()})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
const reply = data.content?.[0]?.text?.trim();
|
|
64
|
+
return `PASS — received "${reply}"`;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return `FAIL (${err.message})`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks that the OpenAI API key is valid by sending a minimal message.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<string>} A PASS, SKIP, or FAIL result string.
|
|
74
|
+
*/
|
|
75
|
+
async function testOpenAI() {
|
|
76
|
+
const key = process.env.OPENAI_API_KEY;
|
|
77
|
+
if (!key) return "SKIP (OPENAI_API_KEY not set)";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Bearer ${key}`,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: process.env.OPENAI_MODEL || "gpt-4o",
|
|
88
|
+
max_tokens: 16,
|
|
89
|
+
messages: [{ role: "user", content: PING_PROMPT }],
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
return `FAIL (HTTP ${response.status}: ${await response.text()})`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
const reply = data.choices?.[0]?.message?.content?.trim();
|
|
99
|
+
return `PASS — received "${reply}"`;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return `FAIL (${err.message})`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Checks that the Perplexity API key is valid by sending a minimal message.
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<string>} A PASS, SKIP, or FAIL result string.
|
|
109
|
+
*/
|
|
110
|
+
async function testPerplexity() {
|
|
111
|
+
const key = process.env.PERPLEXITY_API_KEY;
|
|
112
|
+
if (!key) return "SKIP (PERPLEXITY_API_KEY not set)";
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch("https://api.perplexity.ai/chat/completions", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Authorization: `Bearer ${key}`,
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
model: process.env.PERPLEXITY_MODEL || "sonar-pro",
|
|
123
|
+
max_tokens: 16,
|
|
124
|
+
messages: [{ role: "user", content: PING_PROMPT }],
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
return `FAIL (HTTP ${response.status}: ${await response.text()})`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const data = await response.json();
|
|
133
|
+
const reply = data.choices?.[0]?.message?.content?.trim();
|
|
134
|
+
return `PASS — received "${reply}"`;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return `FAIL (${err.message})`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// File I/O check
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Verifies that the process can write a temporary file, read it back,
|
|
146
|
+
* and clean up after itself. This confirms the file attachment feature
|
|
147
|
+
* of modelmux will work correctly at runtime.
|
|
148
|
+
*
|
|
149
|
+
* @returns {string} A PASS or FAIL result string.
|
|
150
|
+
*/
|
|
151
|
+
function testFileIO() {
|
|
152
|
+
const tmpPath = `/tmp/modelmux-io-test-${Date.now()}.js`;
|
|
153
|
+
const content = 'console.log("modelmux file I/O test");';
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
writeFileSync(tmpPath, content, "utf8");
|
|
157
|
+
const readBack = readFileSync(tmpPath, "utf8");
|
|
158
|
+
unlinkSync(tmpPath);
|
|
159
|
+
|
|
160
|
+
if (!readBack.includes("modelmux file I/O test")) {
|
|
161
|
+
return "FAIL (content mismatch after round-trip)";
|
|
162
|
+
}
|
|
163
|
+
return "PASS — temporary file written, read, and removed";
|
|
164
|
+
} catch (err) {
|
|
165
|
+
// Attempt cleanup even if the test itself failed.
|
|
166
|
+
try { unlinkSync(tmpPath); } catch { /* already gone or never created */ }
|
|
167
|
+
return `FAIL (${err.message})`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Run all checks and report
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
console.log("modelmux — connectivity and file I/O check\n");
|
|
176
|
+
|
|
177
|
+
// Run all API checks concurrently to keep the total wait time short.
|
|
178
|
+
const [claude, openai, perplexity] = await Promise.all([
|
|
179
|
+
testClaude(),
|
|
180
|
+
testOpenAI(),
|
|
181
|
+
testPerplexity(),
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// File I/O is synchronous and fast — run it after the async checks.
|
|
185
|
+
const fileIO = testFileIO();
|
|
186
|
+
|
|
187
|
+
console.log(` Claude → ${claude}`);
|
|
188
|
+
console.log(` OpenAI → ${openai}`);
|
|
189
|
+
console.log(` Perplexity → ${perplexity}`);
|
|
190
|
+
console.log(` File I/O → ${fileIO}`);
|
|
191
|
+
console.log("");
|
|
192
|
+
|
|
193
|
+
const allPassed = [claude, openai, perplexity, fileIO]
|
|
194
|
+
.every((result) => result.startsWith("PASS") || result.startsWith("SKIP"));
|
|
195
|
+
|
|
196
|
+
if (allPassed) {
|
|
197
|
+
console.log("✓ All checks passed. modelmux is ready to use.\n");
|
|
198
|
+
console.log(" File types modelmux accepts by path:");
|
|
199
|
+
console.log(" Claude and OpenAI → code, text, images (.png .jpg .jpeg .gif .webp), PDFs");
|
|
200
|
+
console.log(" Perplexity → code and text files only\n");
|
|
201
|
+
console.log(" Example prompts (use inside Claude Code or Codex):");
|
|
202
|
+
console.log(' "Ask Claude to review /path/to/auth.php for security vulnerabilities"');
|
|
203
|
+
console.log(' "Use the broker to analyse /path/to/diagram.png and suggest improvements"');
|
|
204
|
+
} else {
|
|
205
|
+
console.log("✗ One or more checks failed. Review the output above and check your API keys.");
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|