@ryukin-dev/pi-featherless-kali 1.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/CHANGELOG.md +15 -0
- package/README.md +136 -0
- package/bin/kaliai.js +146 -0
- package/extensions/featherless.ts +16 -0
- package/package.json +71 -0
- package/skills/kali-admin/SKILL.md +30 -0
- package/skills/websearch/SKILL.md +43 -0
- package/skills/websearch/extract.js +65 -0
- package/skills/websearch/package.json +16 -0
- package/skills/websearch/search.js +110 -0
- package/src/handlers/compaction.ts +66 -0
- package/src/handlers/concurrency.ts +70 -0
- package/src/handlers/context.test.ts +260 -0
- package/src/handlers/context.ts +211 -0
- package/src/handlers/provider.ts +14 -0
- package/src/handlers/shared.ts +10 -0
- package/src/handlers/update-check.ts +202 -0
- package/src/models/fetch.ts +31 -0
- package/src/models.ts +262 -0
- package/src/test-api.ts +157 -0
- package/src/tokenize.ts +198 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const PACKAGE_JSON = new URL("../../package.json", import.meta.url);
|
|
6
|
+
const CHANGELOG = new URL("../../CHANGELOG.md", import.meta.url);
|
|
7
|
+
const AGENT_DIR = `${homedir()}/.pi/agent`;
|
|
8
|
+
const LAST_UPDATE_FILE = `${AGENT_DIR}/.kali-ai-last-update`;
|
|
9
|
+
|
|
10
|
+
function getCurrentVersion(): string {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, "utf8"));
|
|
12
|
+
return pkg.version as string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseVersion(version: string): number[] {
|
|
16
|
+
return version
|
|
17
|
+
.replace(/^v/, "")
|
|
18
|
+
.split(".")
|
|
19
|
+
.map((part) => parseInt(part, 10));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isNewer(a: string, b: string): boolean {
|
|
23
|
+
const av = parseVersion(a);
|
|
24
|
+
const bv = parseVersion(b);
|
|
25
|
+
const len = Math.max(av.length, bv.length);
|
|
26
|
+
for (let i = 0; i < len; i++) {
|
|
27
|
+
const ai = av[i] ?? 0;
|
|
28
|
+
const bi = bv[i] ?? 0;
|
|
29
|
+
if (ai > bi) return true;
|
|
30
|
+
if (ai < bi) return false;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(readFileSync(PACKAGE_JSON, "utf8"));
|
|
38
|
+
const name = encodeURIComponent(pkg.name as string);
|
|
39
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/latest`, {
|
|
40
|
+
signal: AbortSignal.timeout(8000),
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) return undefined;
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return typeof data.version === "string" ? data.version : undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readLatestChangelog(): string {
|
|
51
|
+
try {
|
|
52
|
+
const text = readFileSync(CHANGELOG, "utf8");
|
|
53
|
+
const lines = text.split("\n");
|
|
54
|
+
const start = lines.findIndex((l) => l.startsWith("## "));
|
|
55
|
+
if (start === -1) return "Kein Changelog vorhanden.";
|
|
56
|
+
const end = lines.findIndex((l, i) => i > start && l.startsWith("## "));
|
|
57
|
+
return lines
|
|
58
|
+
.slice(start, end === -1 ? undefined : end)
|
|
59
|
+
.join("\n")
|
|
60
|
+
.trim();
|
|
61
|
+
} catch {
|
|
62
|
+
return "Changelog nicht lesbar.";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function bannerLines(version: string, lines: string[]): string[] {
|
|
67
|
+
return [
|
|
68
|
+
`KaliAI v${version}`,
|
|
69
|
+
...lines,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readLastShownVersion(): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(LAST_UPDATE_FILE)) return undefined;
|
|
76
|
+
const data = JSON.parse(readFileSync(LAST_UPDATE_FILE, "utf8"));
|
|
77
|
+
return typeof data.version === "string" ? data.version : undefined;
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeLastShownVersion(version: string): void {
|
|
84
|
+
try {
|
|
85
|
+
if (!existsSync(AGENT_DIR)) mkdirSync(AGENT_DIR, { recursive: true });
|
|
86
|
+
writeFileSync(
|
|
87
|
+
LAST_UPDATE_FILE,
|
|
88
|
+
JSON.stringify({ version }, null, 2),
|
|
89
|
+
"utf8",
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
// ignore
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function registerUpdateCheck(pi: ExtensionAPI) {
|
|
97
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
98
|
+
if (!ctx.ui) return;
|
|
99
|
+
|
|
100
|
+
const current = getCurrentVersion();
|
|
101
|
+
const latest = await fetchLatestVersion();
|
|
102
|
+
const outdated = latest ? isNewer(latest, current) : false;
|
|
103
|
+
|
|
104
|
+
if (outdated && latest) {
|
|
105
|
+
ctx.ui.setWidget(
|
|
106
|
+
"kali-update-banner",
|
|
107
|
+
bannerLines(current, [
|
|
108
|
+
`Update verfügbar: v${latest}`,
|
|
109
|
+
`Installieren mit /update`,
|
|
110
|
+
`Neuigkeiten mit /whatsnew`,
|
|
111
|
+
]),
|
|
112
|
+
{ placement: "aboveEditor" },
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lastShown = readLastShownVersion();
|
|
118
|
+
if (lastShown !== current) {
|
|
119
|
+
writeLastShownVersion(current);
|
|
120
|
+
ctx.ui.setWidget(
|
|
121
|
+
"kali-update-banner",
|
|
122
|
+
bannerLines(current, [
|
|
123
|
+
"Update installiert.",
|
|
124
|
+
...readLatestChangelog().split("\n").slice(0, 12),
|
|
125
|
+
]),
|
|
126
|
+
{ placement: "aboveEditor" },
|
|
127
|
+
);
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
ctx.ui.setWidget("kali-update-banner", undefined);
|
|
130
|
+
}, 10000);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
pi.registerCommand("update", {
|
|
135
|
+
description: "KaliAI auf die neueste npm-Version aktualisieren.",
|
|
136
|
+
handler: async (_args, ctx) => {
|
|
137
|
+
if (!ctx.ui) return;
|
|
138
|
+
const current = getCurrentVersion();
|
|
139
|
+
const latest = await fetchLatestVersion();
|
|
140
|
+
if (!latest) {
|
|
141
|
+
ctx.ui.notify("Versionsprüfung fehlgeschlagen.", "error");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isNewer(latest, current)) {
|
|
145
|
+
ctx.ui.notify(`KaliAI ist aktuell (v${current}).`, "info");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const ok = await ctx.ui.confirm(
|
|
149
|
+
"KaliAI aktualisieren",
|
|
150
|
+
`v${current} -> v${latest} installieren?`,
|
|
151
|
+
);
|
|
152
|
+
if (!ok) return;
|
|
153
|
+
|
|
154
|
+
ctx.ui.setWorkingMessage("KaliAI wird aktualisiert...");
|
|
155
|
+
const result = await pi.exec(
|
|
156
|
+
"npm",
|
|
157
|
+
["install", "-g", `@earendil-works/pi-featherless-kali@${latest}`],
|
|
158
|
+
{ timeout: 120000 },
|
|
159
|
+
);
|
|
160
|
+
ctx.ui.setWorkingMessage();
|
|
161
|
+
|
|
162
|
+
if (result.code !== 0) {
|
|
163
|
+
ctx.ui.notify(
|
|
164
|
+
`Update fehlgeschlagen: ${result.stderr || result.stdout}`,
|
|
165
|
+
"error",
|
|
166
|
+
);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
writeLastShownVersion(latest);
|
|
171
|
+
ctx.ui.setWidget(
|
|
172
|
+
"kali-update-banner",
|
|
173
|
+
bannerLines(latest, [
|
|
174
|
+
"Update installiert.",
|
|
175
|
+
"Bitte KaliAI neu starten.",
|
|
176
|
+
...readLatestChangelog().split("\n").slice(0, 8),
|
|
177
|
+
]),
|
|
178
|
+
{ placement: "aboveEditor" },
|
|
179
|
+
);
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
ctx.ui.setWidget("kali-update-banner", undefined);
|
|
182
|
+
}, 10000);
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
pi.registerCommand("whatsnew", {
|
|
187
|
+
description: "Zeigt die neuesten KaliAI-Änderungen an.",
|
|
188
|
+
handler: async (_args, ctx) => {
|
|
189
|
+
if (!ctx.ui) return;
|
|
190
|
+
const current = getCurrentVersion();
|
|
191
|
+
const latest = await fetchLatestVersion();
|
|
192
|
+
const title = latest
|
|
193
|
+
? `KaliAI v${current} (aktuellste npm: v${latest})`
|
|
194
|
+
: `KaliAI v${current}`;
|
|
195
|
+
ctx.ui.setWidget(
|
|
196
|
+
"kali-update-banner",
|
|
197
|
+
[title, "", ...readLatestChangelog().split("\n")],
|
|
198
|
+
{ placement: "aboveEditor" },
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://api.featherless.ai/v1";
|
|
4
|
+
|
|
5
|
+
export async function fetchModelsJson(): Promise<string> {
|
|
6
|
+
const response = await fetch(`${BASE_URL}/models`, {
|
|
7
|
+
credentials: "include",
|
|
8
|
+
headers: {
|
|
9
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:149.0) Gecko/20100101 Firefox/149.0",
|
|
10
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
11
|
+
"Accept-Language": "en-US",
|
|
12
|
+
"Upgrade-Insecure-Requests": "1",
|
|
13
|
+
"Sec-Fetch-Dest": "document",
|
|
14
|
+
"Sec-Fetch-Mode": "navigate",
|
|
15
|
+
"Sec-Fetch-Site": "none",
|
|
16
|
+
"Sec-Fetch-User": "?1",
|
|
17
|
+
"Priority": "u=0, i",
|
|
18
|
+
"Pragma": "no-cache",
|
|
19
|
+
"Cache-Control": "no-cache",
|
|
20
|
+
},
|
|
21
|
+
method: "GET",
|
|
22
|
+
mode: "cors",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
return JSON.stringify(data, null, 2) + '\n';
|
|
31
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
interface ModelClass {
|
|
4
|
+
context_limit: number;
|
|
5
|
+
concurrency_cost: number;
|
|
6
|
+
chars_per_token?: number;
|
|
7
|
+
cost: {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
cacheRead: number;
|
|
11
|
+
cacheWrite: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MODEL_CLASSES: Record<string, ModelClass> = {
|
|
16
|
+
"glm4-9b": {
|
|
17
|
+
context_limit: 32768,
|
|
18
|
+
concurrency_cost: 1,
|
|
19
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
20
|
+
},
|
|
21
|
+
"qwen25-7b": {
|
|
22
|
+
context_limit: 32768,
|
|
23
|
+
concurrency_cost: 1,
|
|
24
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
25
|
+
},
|
|
26
|
+
"qwen25-3b": {
|
|
27
|
+
context_limit: 32768,
|
|
28
|
+
concurrency_cost: 1,
|
|
29
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
30
|
+
},
|
|
31
|
+
"glm4-32b": {
|
|
32
|
+
context_limit: 32768,
|
|
33
|
+
concurrency_cost: 2,
|
|
34
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
35
|
+
},
|
|
36
|
+
"glm47-flash": {
|
|
37
|
+
context_limit: 32768,
|
|
38
|
+
concurrency_cost: 2,
|
|
39
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
40
|
+
},
|
|
41
|
+
"glm47-357b": {
|
|
42
|
+
context_limit: 32768,
|
|
43
|
+
concurrency_cost: 4,
|
|
44
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
45
|
+
},
|
|
46
|
+
"glm51-754b": {
|
|
47
|
+
context_limit: 32768,
|
|
48
|
+
concurrency_cost: 4,
|
|
49
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
50
|
+
},
|
|
51
|
+
"glm5-754b": {
|
|
52
|
+
context_limit: 32768,
|
|
53
|
+
concurrency_cost: 4,
|
|
54
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
55
|
+
},
|
|
56
|
+
"slf-dstl-1.5b": {
|
|
57
|
+
context_limit: 32768,
|
|
58
|
+
concurrency_cost: 1,
|
|
59
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
60
|
+
},
|
|
61
|
+
"tiny-agent-1.5b": {
|
|
62
|
+
context_limit: 32768,
|
|
63
|
+
concurrency_cost: 1,
|
|
64
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
65
|
+
},
|
|
66
|
+
"minimax-m25": {
|
|
67
|
+
context_limit: 32768,
|
|
68
|
+
concurrency_cost: 4,
|
|
69
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
70
|
+
},
|
|
71
|
+
"kimi-k2": {
|
|
72
|
+
context_limit: 32768,
|
|
73
|
+
concurrency_cost: 4,
|
|
74
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
75
|
+
},
|
|
76
|
+
"kimi-k25": {
|
|
77
|
+
context_limit: 32768,
|
|
78
|
+
concurrency_cost: 4,
|
|
79
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
80
|
+
},
|
|
81
|
+
"deepseek-v3.2": {
|
|
82
|
+
context_limit: 32768,
|
|
83
|
+
concurrency_cost: 4,
|
|
84
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
85
|
+
},
|
|
86
|
+
"deepseek31-685b": {
|
|
87
|
+
context_limit: 32768,
|
|
88
|
+
concurrency_cost: 4,
|
|
89
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
90
|
+
},
|
|
91
|
+
"mistral-24b-2503": {
|
|
92
|
+
context_limit: 32768,
|
|
93
|
+
concurrency_cost: 2,
|
|
94
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
95
|
+
},
|
|
96
|
+
"qwen3-32b": {
|
|
97
|
+
context_limit: 32768,
|
|
98
|
+
concurrency_cost: 2,
|
|
99
|
+
chars_per_token: 3.12,
|
|
100
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
101
|
+
},
|
|
102
|
+
"qwen3-235b": {
|
|
103
|
+
context_limit: 32768,
|
|
104
|
+
concurrency_cost: 4,
|
|
105
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
106
|
+
},
|
|
107
|
+
"qwen3-coder-480b": {
|
|
108
|
+
context_limit: 32768,
|
|
109
|
+
concurrency_cost: 4,
|
|
110
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
111
|
+
},
|
|
112
|
+
"nemotron3-120b": {
|
|
113
|
+
context_limit: 32768,
|
|
114
|
+
concurrency_cost: 4,
|
|
115
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
116
|
+
},
|
|
117
|
+
"qrwkv-72b-32k": {
|
|
118
|
+
context_limit: 65536,
|
|
119
|
+
concurrency_cost: 1,
|
|
120
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
121
|
+
},
|
|
122
|
+
"qrwkv-32b-32k": {
|
|
123
|
+
context_limit: 32768,
|
|
124
|
+
concurrency_cost: 1,
|
|
125
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export interface ModelEntry {
|
|
130
|
+
id: string;
|
|
131
|
+
model_class: string;
|
|
132
|
+
reasoning?: boolean;
|
|
133
|
+
tool_use?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const MODELS: ModelEntry[] = [
|
|
137
|
+
{
|
|
138
|
+
id: "zai-org/GLM-Z1-9B-0414",
|
|
139
|
+
model_class: "glm4-9b",
|
|
140
|
+
tool_use: true,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "Qwen/Qwen2.5-Coder-7B-Instruct",
|
|
144
|
+
model_class: "qwen25-7b",
|
|
145
|
+
tool_use: true,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "Qwen/Qwen2.5-3B-Instruct",
|
|
149
|
+
model_class: "qwen25-3b",
|
|
150
|
+
tool_use: true,
|
|
151
|
+
},
|
|
152
|
+
{ id: "zai-org/GLM-4.7-Flash", model_class: "glm47-flash", tool_use: true },
|
|
153
|
+
{ id: "RyanYr/slf-dstl_Q2.5-1.5B-It_tooluse_SFT", model_class: "slf-dstl-1.5b", tool_use: true },
|
|
154
|
+
{ id: "driaforall/Tiny-Agent-a-1.5B", model_class: "tiny-agent-1.5b", tool_use: true },
|
|
155
|
+
{ id: "zai-org/GLM-4.7", model_class: "glm47-357b", tool_use: true },
|
|
156
|
+
{ id: "zai-org/GLM-5", model_class: "glm51-754b", tool_use: true },
|
|
157
|
+
{ id: "zai-org/GLM-5", model_class: "glm5-754b", tool_use: true },
|
|
158
|
+
{
|
|
159
|
+
id: "MiniMaxAI/MiniMax-M2.5",
|
|
160
|
+
model_class: "minimax-m25",
|
|
161
|
+
tool_use: true,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "moonshotai/Kimi-K2-Instruct",
|
|
165
|
+
model_class: "kimi-k2",
|
|
166
|
+
tool_use: true,
|
|
167
|
+
},
|
|
168
|
+
{ id: "moonshotai/Kimi-K2.5", model_class: "kimi-k25", tool_use: true },
|
|
169
|
+
{
|
|
170
|
+
id: "deepseek-ai/DeepSeek-V3.2",
|
|
171
|
+
model_class: "deepseek-v3.2",
|
|
172
|
+
tool_use: true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "deepseek-ai/DeepSeek-V3.1",
|
|
176
|
+
model_class: "deepseek31-685b",
|
|
177
|
+
tool_use: true,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "mistralai/Mistral-Small-3.2-24B-Instruct-2506",
|
|
181
|
+
model_class: "mistral-24b-2503",
|
|
182
|
+
tool_use: true,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: "Qwen/Qwen3-32B",
|
|
186
|
+
model_class: "qwen3-32b",
|
|
187
|
+
reasoning: true,
|
|
188
|
+
tool_use: true,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "Qwen/Qwen3-235B-A22B",
|
|
192
|
+
model_class: "qwen3-235b",
|
|
193
|
+
reasoning: true,
|
|
194
|
+
tool_use: true,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: "Qwen/Qwen3-Coder-480B-A35B-Instruct",
|
|
198
|
+
model_class: "qwen3-coder-480b",
|
|
199
|
+
reasoning: true,
|
|
200
|
+
tool_use: true,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16",
|
|
204
|
+
model_class: "nemotron3-120b",
|
|
205
|
+
tool_use: true,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "recursal/RWKV6Qwen2.5-32B-QwQ-Preview",
|
|
209
|
+
model_class: "qrwkv-32b-32k",
|
|
210
|
+
tool_use: true,
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
export function getModelConfig(entry: ModelEntry) {
|
|
215
|
+
const mc = MODEL_CLASSES[entry.model_class];
|
|
216
|
+
if (!mc) throw new Error(`Unknown model_class: ${entry.model_class}`);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
id: entry.id,
|
|
220
|
+
name: entry.id,
|
|
221
|
+
reasoning: entry.reasoning ?? false,
|
|
222
|
+
contextWindow: mc.context_limit,
|
|
223
|
+
maxTokens: 131072, // Support infinite generation beyond context window
|
|
224
|
+
input: ["text"] as ("text" | "image")[],
|
|
225
|
+
cost: mc.cost,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getRealContextLimit(modelId: string): number | undefined {
|
|
230
|
+
const entry = MODELS.find((m) => m.id === modelId);
|
|
231
|
+
if (!entry) return undefined;
|
|
232
|
+
const mc = MODEL_CLASSES[entry.model_class];
|
|
233
|
+
return mc?.context_limit;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function getCharsPerToken(modelClass: string): number {
|
|
237
|
+
const mc = MODEL_CLASSES[modelClass];
|
|
238
|
+
return mc?.chars_per_token ?? 3.2;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getConcurrencyCost(modelClass: string): number {
|
|
242
|
+
const mc = MODEL_CLASSES[modelClass];
|
|
243
|
+
if (!mc) throw new Error(`Unknown model_class: ${modelClass}`);
|
|
244
|
+
return mc.concurrency_cost;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function getModelClass(modelId: string): string | undefined {
|
|
248
|
+
const entry = MODELS.find((m) => m.id === modelId);
|
|
249
|
+
if (entry) return entry.model_class;
|
|
250
|
+
|
|
251
|
+
const lower = modelId.toLowerCase();
|
|
252
|
+
if (lower.includes("qrwkv-72b") || lower.includes("qrwkv7b-72b")) {
|
|
253
|
+
return "qrwkv-72b-32k";
|
|
254
|
+
}
|
|
255
|
+
if (lower.includes("qrwkv-32b") || lower.includes("qrwkv7b-32b")) {
|
|
256
|
+
return "qrwkv-32b-32k";
|
|
257
|
+
}
|
|
258
|
+
if (lower.includes("qwen3-32b")) {
|
|
259
|
+
return "qwen3-32b";
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
package/src/test-api.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const TIMEOUT_MS = 20000;
|
|
4
|
+
const BASE_URL = "https://api.featherless.ai/v1";
|
|
5
|
+
const MODEL_ID = "zai-org/GLM-5";
|
|
6
|
+
|
|
7
|
+
async function withTimeout<T>(promise: Promise<T>, ms: number = TIMEOUT_MS): Promise<T> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
10
|
+
promise.then(
|
|
11
|
+
(result) => { clearTimeout(timer); resolve(result); },
|
|
12
|
+
(error) => { clearTimeout(timer); reject(error); }
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function testPing(apiKey: string) {
|
|
18
|
+
console.log("🏓 Pinging API (simple chat)...");
|
|
19
|
+
const start = Date.now();
|
|
20
|
+
const response = await withTimeout(
|
|
21
|
+
fetch(`${BASE_URL}/chat/completions`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
25
|
+
"Content-Type": "application/json"
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
model: MODEL_ID,
|
|
29
|
+
messages: [{ role: "user", content: "Reply with: pong" }],
|
|
30
|
+
max_tokens: 10
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
const elapsed = Date.now() - start;
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
throw new Error(`API error ${response.status}: ${text.slice(0, 200)}`);
|
|
38
|
+
}
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const usage = data.usage || {};
|
|
41
|
+
console.log(`✅ API reachable in ${elapsed}ms`);
|
|
42
|
+
console.log(` Model: ${MODEL_ID}`);
|
|
43
|
+
console.log(` Usage: ${JSON.stringify(usage)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function testChat(apiKey: string) {
|
|
47
|
+
console.log("🤖 Testing chat completion...");
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
const response = await withTimeout(
|
|
50
|
+
fetch(`${BASE_URL}/chat/completions`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
54
|
+
"Content-Type": "application/json"
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: MODEL_ID,
|
|
58
|
+
messages: [{ role: "user", content: "Say 'hello' and nothing else." }],
|
|
59
|
+
max_tokens: 50
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
const elapsed = Date.now() - start;
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
throw new Error(`Request failed ${response.status}: ${text.slice(0, 200)}`);
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
const content = data.choices?.[0]?.message?.content || "(no content)";
|
|
70
|
+
const reasoning = data.choices?.[0]?.message?.reasoning || "";
|
|
71
|
+
const usage = data.usage || {};
|
|
72
|
+
console.log(`✅ Chat completion succeeded in ${elapsed}ms`);
|
|
73
|
+
console.log(` Model: ${MODEL_ID}`);
|
|
74
|
+
console.log(` Response: "${content}"`);
|
|
75
|
+
if (reasoning) console.log(` Reasoning: "${reasoning.slice(0, 100)}..."`);
|
|
76
|
+
console.log(` Usage: ${JSON.stringify(usage)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function testTools(apiKey: string) {
|
|
80
|
+
console.log("🔧 Testing tool calling...");
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
const response = await withTimeout(
|
|
83
|
+
fetch(`${BASE_URL}/chat/completions`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
87
|
+
"Content-Type": "application/json"
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
model: MODEL_ID,
|
|
91
|
+
messages: [{ role: "user", content: "What is 2+2? Use the calculator tool." }],
|
|
92
|
+
max_tokens: 100,
|
|
93
|
+
tools: [{
|
|
94
|
+
type: "function",
|
|
95
|
+
function: {
|
|
96
|
+
name: "calculator",
|
|
97
|
+
description: "Perform arithmetic",
|
|
98
|
+
parameters: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
expression: { type: "string", description: "Math expression" }
|
|
102
|
+
},
|
|
103
|
+
required: ["expression"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}]
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
const elapsed = Date.now() - start;
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const text = await response.text();
|
|
113
|
+
throw new Error(`Request failed ${response.status}: ${text.slice(0, 200)}`);
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
const choice = data.choices?.[0];
|
|
117
|
+
const message = choice?.message || {};
|
|
118
|
+
const toolCalls = message.tool_calls || [];
|
|
119
|
+
console.log(`✅ Tool calling test completed in ${elapsed}ms`);
|
|
120
|
+
console.log(` Model: ${MODEL_ID}`);
|
|
121
|
+
console.log(` Tool calls: ${toolCalls.length}`);
|
|
122
|
+
console.log(` Content: "${message.content || "(none)"}"`);
|
|
123
|
+
if (toolCalls.length > 0) {
|
|
124
|
+
console.log(` Tool calls: ${JSON.stringify(toolCalls, null, 2)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function main() {
|
|
129
|
+
const apiKey = process.env.FEATHERLESS_API_KEY;
|
|
130
|
+
if (!apiKey) {
|
|
131
|
+
console.error("❌ FEATHERLESS_API_KEY not set");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const command = process.argv[2] || "ping";
|
|
135
|
+
try {
|
|
136
|
+
switch (command) {
|
|
137
|
+
case "ping":
|
|
138
|
+
await testPing(apiKey);
|
|
139
|
+
break;
|
|
140
|
+
case "chat":
|
|
141
|
+
await testChat(apiKey);
|
|
142
|
+
break;
|
|
143
|
+
case "tools":
|
|
144
|
+
await testTools(apiKey);
|
|
145
|
+
break;
|
|
146
|
+
default:
|
|
147
|
+
console.error(`Unknown command: ${command}`);
|
|
148
|
+
console.error("Usage: npx tsx src/test-api.ts [ping|chat|tools]");
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error(`❌ Test failed: ${error}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
main();
|