pi-freeflow 1.2.0 → 1.3.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 +21 -0
- package/README.md +7 -11
- package/extensions/index.ts +4 -2137
- package/package.json +30 -5
- package/src/catalog.ts +204 -0
- package/src/commands.ts +448 -0
- package/src/config.ts +145 -0
- package/src/deploy.ts +126 -0
- package/src/index.ts +244 -0
- package/src/logger.ts +327 -0
- package/src/models.ts +343 -0
- package/src/proxy.ts +473 -0
- package/src/rate-limiter.ts +148 -0
- package/src/relay-state.ts +218 -0
- package/src/relay.ts +193 -0
- package/src/stream-pipe.ts +154 -0
- package/src/types.ts +164 -0
package/src/logger.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, leveled, rotating, request-aware logger for pi-freeflow
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
DEBUG_STATE_FILE,
|
|
10
|
+
LOG_FILE,
|
|
11
|
+
LOG_MAX_BYTES,
|
|
12
|
+
LOG_MAX_FILES,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import type { DebugState, LogLevel } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export const LOG_LEVEL_ORDER: Record<LogLevel, number> = {
|
|
17
|
+
debug: 0,
|
|
18
|
+
info: 1,
|
|
19
|
+
warn: 2,
|
|
20
|
+
error: 3,
|
|
21
|
+
audit: 4,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let cachedDebugState: DebugState | null | undefined = undefined;
|
|
25
|
+
let cachedDebugMtime = 0;
|
|
26
|
+
let cachedDebugAt = 0;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Load persisted debug state from disk with a 1-second in-memory mtime cache.
|
|
30
|
+
*/
|
|
31
|
+
export function loadDebugState(): DebugState | null {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (cachedDebugState !== undefined && now - cachedDebugAt < 1000) {
|
|
34
|
+
return cachedDebugState;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(DEBUG_STATE_FILE)) {
|
|
39
|
+
cachedDebugState = null;
|
|
40
|
+
cachedDebugAt = now;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stat = fs.statSync(DEBUG_STATE_FILE);
|
|
45
|
+
if (stat.mtimeMs === cachedDebugMtime && cachedDebugState !== undefined) {
|
|
46
|
+
cachedDebugAt = now;
|
|
47
|
+
return cachedDebugState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = fs.readFileSync(DEBUG_STATE_FILE, "utf8");
|
|
51
|
+
const parsed = JSON.parse(raw) as DebugState;
|
|
52
|
+
if (typeof parsed?.debug === "boolean") {
|
|
53
|
+
cachedDebugState = parsed;
|
|
54
|
+
cachedDebugMtime = stat.mtimeMs;
|
|
55
|
+
cachedDebugAt = now;
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Non-fatal if parsing or reading fails
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
cachedDebugState = null;
|
|
63
|
+
cachedDebugAt = now;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Atomically persist debug state to disk.
|
|
69
|
+
*/
|
|
70
|
+
export function saveDebugState(s: DebugState): void {
|
|
71
|
+
try {
|
|
72
|
+
const dir = path.dirname(DEBUG_STATE_FILE);
|
|
73
|
+
if (!fs.existsSync(dir)) {
|
|
74
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
const tmp = `${DEBUG_STATE_FILE}.${randomUUID()}.tmp`;
|
|
77
|
+
fs.writeFileSync(tmp, JSON.stringify(s, null, 2), "utf8");
|
|
78
|
+
fs.renameSync(tmp, DEBUG_STATE_FILE);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const stat = fs.statSync(DEBUG_STATE_FILE);
|
|
82
|
+
cachedDebugState = s;
|
|
83
|
+
cachedDebugMtime = stat.mtimeMs;
|
|
84
|
+
cachedDebugAt = Date.now();
|
|
85
|
+
} catch {
|
|
86
|
+
cachedDebugState = s;
|
|
87
|
+
cachedDebugAt = Date.now();
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Avoid recursive logger calls on save failure
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Calculate the active minimum log level threshold based on state and env.
|
|
96
|
+
*/
|
|
97
|
+
export function getMinLogLevel(): number {
|
|
98
|
+
const dbg = loadDebugState();
|
|
99
|
+
if (dbg?.debug) {
|
|
100
|
+
return LOG_LEVEL_ORDER.debug;
|
|
101
|
+
}
|
|
102
|
+
if (dbg?.level && dbg.level in LOG_LEVEL_ORDER) {
|
|
103
|
+
return LOG_LEVEL_ORDER[dbg.level];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const raw = (process.env.FREEFLOW_LOG_LEVEL || "info").toLowerCase();
|
|
107
|
+
|
|
108
|
+
if (raw in LOG_LEVEL_ORDER) {
|
|
109
|
+
return LOG_LEVEL_ORDER[raw as LogLevel];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isEnvDebug =
|
|
113
|
+
process.env.FREEFLOW_DEBUG === "1" ||
|
|
114
|
+
process.env.FREEFLOW_DEBUG === "true";
|
|
115
|
+
|
|
116
|
+
if (isEnvDebug) {
|
|
117
|
+
return LOG_LEVEL_ORDER.debug;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return LOG_LEVEL_ORDER.info;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function shouldLog(level: LogLevel): boolean {
|
|
124
|
+
return LOG_LEVEL_ORDER[level] >= getMinLogLevel();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isDebugEnabled(): boolean {
|
|
128
|
+
return LOG_LEVEL_ORDER.debug >= getMinLogLevel();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Rotate log files if current log file size exceeds LOG_MAX_BYTES.
|
|
133
|
+
* Rotates: log -> log.1 -> log.2 -> log.3 ... up to LOG_MAX_FILES.
|
|
134
|
+
*/
|
|
135
|
+
export function rotateLogsIfNeeded(targetFile: string = LOG_FILE): void {
|
|
136
|
+
try {
|
|
137
|
+
if (!fs.existsSync(targetFile)) return;
|
|
138
|
+
const stat = fs.statSync(targetFile);
|
|
139
|
+
if (stat.size <= LOG_MAX_BYTES) return;
|
|
140
|
+
|
|
141
|
+
for (let i = LOG_MAX_FILES; i >= 1; i--) {
|
|
142
|
+
const src = i === 1 ? targetFile : `${targetFile}.${i - 1}`;
|
|
143
|
+
const dst = `${targetFile}.${i}`;
|
|
144
|
+
try {
|
|
145
|
+
if (fs.existsSync(src)) {
|
|
146
|
+
if (i === LOG_MAX_FILES && fs.existsSync(dst)) {
|
|
147
|
+
fs.unlinkSync(dst);
|
|
148
|
+
} else if (fs.existsSync(dst)) {
|
|
149
|
+
fs.unlinkSync(dst);
|
|
150
|
+
}
|
|
151
|
+
fs.renameSync(src, dst);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// Ignore rotation step error and continue
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore rotation errors
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Safely format metadata object and optional requestId for log output.
|
|
164
|
+
*/
|
|
165
|
+
export function formatLogMeta(
|
|
166
|
+
meta?: Record<string, unknown>,
|
|
167
|
+
reqId?: string,
|
|
168
|
+
): string {
|
|
169
|
+
const parts: string[] = [];
|
|
170
|
+
if (reqId) {
|
|
171
|
+
parts.push(`req=${reqId}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
175
|
+
const safe: Record<string, unknown> = {};
|
|
176
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
177
|
+
if (typeof v === "string" && v.length > 800) {
|
|
178
|
+
safe[k] = `${v.slice(0, 800)}…(${v.length})`;
|
|
179
|
+
} else if (v instanceof Error) {
|
|
180
|
+
safe[k] = {
|
|
181
|
+
name: v.name,
|
|
182
|
+
message: v.message,
|
|
183
|
+
stack: v.stack?.split("\n").slice(0, 3).join(" | "),
|
|
184
|
+
};
|
|
185
|
+
} else {
|
|
186
|
+
safe[k] = v;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
parts.push(JSON.stringify(safe));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Write a structured log entry to disk if level passes threshold.
|
|
197
|
+
*/
|
|
198
|
+
export function log(
|
|
199
|
+
level: LogLevel,
|
|
200
|
+
message: string,
|
|
201
|
+
meta?: Record<string, unknown>,
|
|
202
|
+
reqId?: string,
|
|
203
|
+
): void {
|
|
204
|
+
if (!shouldLog(level)) return;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const ts = new Date().toISOString();
|
|
208
|
+
const reqTag = reqId ? ` [${reqId}]` : "";
|
|
209
|
+
const line = `[${ts}] [${level.toUpperCase()}]${reqTag} ${message}${formatLogMeta(meta)}\n`;
|
|
210
|
+
|
|
211
|
+
rotateLogsIfNeeded(LOG_FILE);
|
|
212
|
+
|
|
213
|
+
const dir = path.dirname(LOG_FILE);
|
|
214
|
+
if (!fs.existsSync(dir)) {
|
|
215
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
216
|
+
}
|
|
217
|
+
fs.appendFileSync(LOG_FILE, line, "utf8");
|
|
218
|
+
} catch {
|
|
219
|
+
// Fallback silent failure
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function logDebug(
|
|
224
|
+
message: string,
|
|
225
|
+
meta?: Record<string, unknown>,
|
|
226
|
+
reqId?: string,
|
|
227
|
+
): void {
|
|
228
|
+
log("debug", message, meta, reqId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function logInfo(
|
|
232
|
+
message: string,
|
|
233
|
+
meta?: Record<string, unknown>,
|
|
234
|
+
reqId?: string,
|
|
235
|
+
): void {
|
|
236
|
+
log("info", message, meta, reqId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function logWarn(
|
|
240
|
+
message: string,
|
|
241
|
+
meta?: Record<string, unknown>,
|
|
242
|
+
reqId?: string,
|
|
243
|
+
): void {
|
|
244
|
+
log("warn", message, meta, reqId);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function logError(
|
|
248
|
+
message: string,
|
|
249
|
+
meta?: Record<string, unknown>,
|
|
250
|
+
reqId?: string,
|
|
251
|
+
): void {
|
|
252
|
+
log("error", message, meta, reqId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function logAudit(
|
|
256
|
+
message: string,
|
|
257
|
+
meta?: Record<string, unknown>,
|
|
258
|
+
reqId?: string,
|
|
259
|
+
): void {
|
|
260
|
+
log("audit", message, meta, reqId);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface ReadRecentLogsResult {
|
|
264
|
+
lines: string[];
|
|
265
|
+
totalMatched: number;
|
|
266
|
+
totalLines: number;
|
|
267
|
+
logFile: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Read recent log entries from log file and its rotated archives.
|
|
272
|
+
*/
|
|
273
|
+
export function readRecentLogs(
|
|
274
|
+
filterLevel?: LogLevel | null,
|
|
275
|
+
filterReqId?: string | null,
|
|
276
|
+
count = 25,
|
|
277
|
+
): ReadRecentLogsResult {
|
|
278
|
+
const files: string[] = [
|
|
279
|
+
LOG_FILE,
|
|
280
|
+
`${LOG_FILE}.1`,
|
|
281
|
+
`${LOG_FILE}.2`,
|
|
282
|
+
`${LOG_FILE}.3`,
|
|
283
|
+
].filter((f) => fs.existsSync(f));
|
|
284
|
+
|
|
285
|
+
if (files.length === 0) {
|
|
286
|
+
return {
|
|
287
|
+
lines: [],
|
|
288
|
+
totalMatched: 0,
|
|
289
|
+
totalLines: 0,
|
|
290
|
+
logFile: LOG_FILE,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let allLines: string[] = [];
|
|
295
|
+
for (const f of files) {
|
|
296
|
+
try {
|
|
297
|
+
const content = fs.readFileSync(f, "utf8");
|
|
298
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
299
|
+
allLines = lines.concat(allLines);
|
|
300
|
+
} catch {
|
|
301
|
+
// Skip unreadable rotated files
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let filtered = allLines;
|
|
306
|
+
if (filterLevel) {
|
|
307
|
+
const levelTag = `[${filterLevel.toUpperCase()}]`;
|
|
308
|
+
filtered = filtered.filter((l) => l.includes(levelTag));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (filterReqId) {
|
|
312
|
+
const cleanedReqId = filterReqId.replace(/^req=/, "");
|
|
313
|
+
filtered = filtered.filter(
|
|
314
|
+
(l) => l.includes(cleanedReqId) || l.includes(`[${cleanedReqId}]`),
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const clampedCount = Math.min(200, Math.max(1, count));
|
|
319
|
+
const resultLines = filtered.slice(-clampedCount);
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
lines: resultLines,
|
|
323
|
+
totalMatched: filtered.length,
|
|
324
|
+
totalLines: allLines.length,
|
|
325
|
+
logFile: LOG_FILE,
|
|
326
|
+
};
|
|
327
|
+
}
|
package/src/models.ts
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static model definitions and upstream routing catalogs for pi-freeflow
|
|
3
|
+
*
|
|
4
|
+
* Defines the 23 verified free models:
|
|
5
|
+
* - 9 OpenCode Zen models (2 Responses API + 7 Chat Completions)
|
|
6
|
+
* - 14 KiloCode Keyless Gateway models (11 OpenRouter format + 3 Standard format)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ModelDef, Upstream } from "./types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* OpenCode Zen free models verified against the live catalog and inference APIs.
|
|
13
|
+
* Endpoint: https://opencode.ai/zen/v1
|
|
14
|
+
*/
|
|
15
|
+
export const OPENCODE_MODELS: ModelDef[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "deepseek-v4-flash-free",
|
|
18
|
+
name: "DeepSeek V4 Flash (1M)",
|
|
19
|
+
reasoning: true,
|
|
20
|
+
contextWindow: 1_000_000,
|
|
21
|
+
maxTokens: 384_000,
|
|
22
|
+
input: ["text"],
|
|
23
|
+
thinkingLevelMap: {
|
|
24
|
+
off: null,
|
|
25
|
+
minimal: "low",
|
|
26
|
+
low: "low",
|
|
27
|
+
medium: "high",
|
|
28
|
+
high: "high",
|
|
29
|
+
xhigh: "max",
|
|
30
|
+
max: "max",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "x-preview-f-free",
|
|
35
|
+
name: "Ox Alpha (1M)",
|
|
36
|
+
reasoning: true,
|
|
37
|
+
contextWindow: 1_048_576,
|
|
38
|
+
maxTokens: 131_072,
|
|
39
|
+
input: ["text", "image"],
|
|
40
|
+
thinkingLevelMap: {
|
|
41
|
+
off: null,
|
|
42
|
+
minimal: "low",
|
|
43
|
+
low: "low",
|
|
44
|
+
medium: "high",
|
|
45
|
+
high: "high",
|
|
46
|
+
xhigh: "max",
|
|
47
|
+
max: "max",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "muse-spark-1.2-contributor-free",
|
|
52
|
+
name: "Muse Spark 1.2 (1M)",
|
|
53
|
+
reasoning: true,
|
|
54
|
+
contextWindow: 1_048_576,
|
|
55
|
+
maxTokens: 131_072,
|
|
56
|
+
api: "openai-responses",
|
|
57
|
+
input: ["text", "image"],
|
|
58
|
+
thinkingLevelMap: {
|
|
59
|
+
off: null,
|
|
60
|
+
minimal: "minimal",
|
|
61
|
+
low: "low",
|
|
62
|
+
medium: "medium",
|
|
63
|
+
high: "high",
|
|
64
|
+
xhigh: "xhigh",
|
|
65
|
+
max: "max",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "mimo-v2.5-free",
|
|
70
|
+
name: "MiMo V2.5 (1M)",
|
|
71
|
+
reasoning: true,
|
|
72
|
+
contextWindow: 1_048_576,
|
|
73
|
+
maxTokens: 131_072,
|
|
74
|
+
input: ["text", "image"],
|
|
75
|
+
thinkingLevelMap: {
|
|
76
|
+
off: null,
|
|
77
|
+
minimal: "low",
|
|
78
|
+
low: "low",
|
|
79
|
+
medium: "medium",
|
|
80
|
+
high: "high",
|
|
81
|
+
xhigh: "high",
|
|
82
|
+
max: "high",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "hy3-free",
|
|
87
|
+
name: "Hy3 (262K)",
|
|
88
|
+
reasoning: true,
|
|
89
|
+
contextWindow: 262_144,
|
|
90
|
+
maxTokens: 128_000,
|
|
91
|
+
input: ["text"],
|
|
92
|
+
thinkingLevelMap: {
|
|
93
|
+
off: null,
|
|
94
|
+
minimal: "low",
|
|
95
|
+
low: "low",
|
|
96
|
+
medium: "high",
|
|
97
|
+
high: "high",
|
|
98
|
+
xhigh: "max",
|
|
99
|
+
max: "max",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "nemotron-3-ultra-free",
|
|
104
|
+
name: "Nemotron 3 Ultra (1M)",
|
|
105
|
+
reasoning: true,
|
|
106
|
+
contextWindow: 1_000_000,
|
|
107
|
+
maxTokens: 128_000,
|
|
108
|
+
input: ["text"],
|
|
109
|
+
thinkingLevelMap: {
|
|
110
|
+
off: null,
|
|
111
|
+
minimal: "low",
|
|
112
|
+
low: "low",
|
|
113
|
+
medium: "high",
|
|
114
|
+
high: "high",
|
|
115
|
+
xhigh: "max",
|
|
116
|
+
max: "max",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "nemotron-3.5-lightning-free",
|
|
121
|
+
name: "Nemotron 3.5 Lightning (1M)",
|
|
122
|
+
reasoning: true,
|
|
123
|
+
contextWindow: 1_000_000,
|
|
124
|
+
maxTokens: 262_144,
|
|
125
|
+
input: ["text"],
|
|
126
|
+
thinkingLevelMap: {
|
|
127
|
+
off: null,
|
|
128
|
+
minimal: "low",
|
|
129
|
+
low: "low",
|
|
130
|
+
medium: "high",
|
|
131
|
+
high: "high",
|
|
132
|
+
xhigh: "max",
|
|
133
|
+
max: "max",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "big-pickle",
|
|
138
|
+
name: "Big Pickle",
|
|
139
|
+
reasoning: true,
|
|
140
|
+
contextWindow: 200_000,
|
|
141
|
+
maxTokens: 32_000,
|
|
142
|
+
input: ["text"],
|
|
143
|
+
thinkingLevelMap: {
|
|
144
|
+
off: null,
|
|
145
|
+
minimal: "high",
|
|
146
|
+
low: "high",
|
|
147
|
+
medium: "high",
|
|
148
|
+
high: "high",
|
|
149
|
+
xhigh: "max",
|
|
150
|
+
max: "max",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "laguna-s-2.1-free",
|
|
155
|
+
name: "Laguna S 2.1 (1M)",
|
|
156
|
+
reasoning: true,
|
|
157
|
+
contextWindow: 1_048_576,
|
|
158
|
+
maxTokens: 131_072,
|
|
159
|
+
input: ["text"],
|
|
160
|
+
thinkingLevelMap: {
|
|
161
|
+
off: null,
|
|
162
|
+
minimal: "low",
|
|
163
|
+
low: "low",
|
|
164
|
+
medium: "high",
|
|
165
|
+
high: "high",
|
|
166
|
+
xhigh: "max",
|
|
167
|
+
max: "max",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Backward compatibility alias for OPENCODE_MODELS
|
|
174
|
+
*/
|
|
175
|
+
export const KNOWN_MODELS = OPENCODE_MODELS;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* KiloCode Gateway free models (keyless — https://kilo.ai/docs/gateway).
|
|
179
|
+
* Endpoint: https://api.kilo.ai/api/gateway/chat/completions
|
|
180
|
+
*/
|
|
181
|
+
export const KILO_MODELS: ModelDef[] = [
|
|
182
|
+
{
|
|
183
|
+
id: "dots-studio/dots-3-note-preview:free",
|
|
184
|
+
name: "Dots3-Note Preview (512K)",
|
|
185
|
+
reasoning: true,
|
|
186
|
+
contextWindow: 512_000,
|
|
187
|
+
maxTokens: 512_000,
|
|
188
|
+
input: ["text", "image"],
|
|
189
|
+
thinkingFormat: "openrouter",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "stepfun/step-3.7-flash:free",
|
|
193
|
+
name: "Step 3.7 Flash",
|
|
194
|
+
reasoning: true,
|
|
195
|
+
contextWindow: 262_144,
|
|
196
|
+
maxTokens: 262_144,
|
|
197
|
+
input: ["text", "image"],
|
|
198
|
+
thinkingFormat: "openrouter",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
|
202
|
+
name: "Nemotron 3 Nano Omni",
|
|
203
|
+
reasoning: true,
|
|
204
|
+
contextWindow: 256_000,
|
|
205
|
+
maxTokens: 65_536,
|
|
206
|
+
input: ["text", "image"],
|
|
207
|
+
thinkingFormat: "openrouter",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
211
|
+
name: "Nemotron 3 Ultra 550B (1M)",
|
|
212
|
+
reasoning: true,
|
|
213
|
+
contextWindow: 1_000_000,
|
|
214
|
+
maxTokens: 65_536,
|
|
215
|
+
input: ["text"],
|
|
216
|
+
thinkingFormat: "openrouter",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "nvidia/nemotron-3.5-lightning:free",
|
|
220
|
+
name: "Nemotron 3.5 Lightning (Kilo)",
|
|
221
|
+
reasoning: true,
|
|
222
|
+
contextWindow: 1_000_000,
|
|
223
|
+
maxTokens: 65_536,
|
|
224
|
+
input: ["text"],
|
|
225
|
+
thinkingFormat: "openrouter",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: "nvidia/nemotron-3-super-120b-a12b:free",
|
|
229
|
+
name: "Nemotron 3 Super 120B",
|
|
230
|
+
reasoning: true,
|
|
231
|
+
contextWindow: 262_144,
|
|
232
|
+
maxTokens: 262_144,
|
|
233
|
+
input: ["text"],
|
|
234
|
+
thinkingFormat: "openrouter",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "tencent/hy3:free",
|
|
238
|
+
name: "Tencent Hy3 (Kilo)",
|
|
239
|
+
reasoning: true,
|
|
240
|
+
contextWindow: 262_144,
|
|
241
|
+
maxTokens: 128_000,
|
|
242
|
+
input: ["text"],
|
|
243
|
+
thinkingFormat: "openrouter",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "cohere/north-mini-code:free",
|
|
247
|
+
name: "North Mini Code",
|
|
248
|
+
reasoning: true,
|
|
249
|
+
contextWindow: 256_000,
|
|
250
|
+
maxTokens: 64_000,
|
|
251
|
+
input: ["text"],
|
|
252
|
+
thinkingFormat: "openrouter",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: "poolside/laguna-s-2.1:free",
|
|
256
|
+
name: "Laguna S 2.1 (Kilo)",
|
|
257
|
+
reasoning: true,
|
|
258
|
+
contextWindow: 262_144,
|
|
259
|
+
maxTokens: 32_768,
|
|
260
|
+
input: ["text"],
|
|
261
|
+
thinkingFormat: "openrouter",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "poolside/laguna-xs-2.1:free",
|
|
265
|
+
name: "Laguna XS 2.1",
|
|
266
|
+
reasoning: true,
|
|
267
|
+
contextWindow: 262_144,
|
|
268
|
+
maxTokens: 32_768,
|
|
269
|
+
input: ["text"],
|
|
270
|
+
thinkingFormat: "openrouter",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
id: "liquid/lfm-2.5-2.6b:free",
|
|
274
|
+
name: "Liquid LFM 2.5",
|
|
275
|
+
reasoning: true,
|
|
276
|
+
contextWindow: 65_536,
|
|
277
|
+
maxTokens: 8_192,
|
|
278
|
+
input: ["text"],
|
|
279
|
+
thinkingFormat: "openrouter",
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "kilo-auto/free",
|
|
283
|
+
name: "Kilo Auto",
|
|
284
|
+
reasoning: false,
|
|
285
|
+
contextWindow: 256_000,
|
|
286
|
+
maxTokens: 10_000,
|
|
287
|
+
input: ["text"],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "openrouter/free",
|
|
291
|
+
name: "OpenRouter Auto",
|
|
292
|
+
reasoning: false,
|
|
293
|
+
contextWindow: 200_000,
|
|
294
|
+
maxTokens: 65_536,
|
|
295
|
+
input: ["text"],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "nvidia/nemotron-3.5-content-safety:free",
|
|
299
|
+
name: "Nemotron Content Safety",
|
|
300
|
+
reasoning: false,
|
|
301
|
+
contextWindow: 128_000,
|
|
302
|
+
maxTokens: 8_192,
|
|
303
|
+
input: ["text"],
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Set of all KiloCode model IDs for fast lookup
|
|
309
|
+
*/
|
|
310
|
+
export const KILO_MODEL_IDS = new Set<string>(KILO_MODELS.map((m) => m.id));
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Combined list of all 23 static free models
|
|
314
|
+
*/
|
|
315
|
+
export const ALL_MODELS: ModelDef[] = [...OPENCODE_MODELS, ...KILO_MODELS];
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Map of model ID -> ModelDef
|
|
319
|
+
*/
|
|
320
|
+
export const MODEL_MAP = new Map<string, ModelDef>(
|
|
321
|
+
ALL_MODELS.map((m) => [m.id, m]),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Lookup a model definition by ID
|
|
326
|
+
*/
|
|
327
|
+
export function getModelDef(id: string): ModelDef | undefined {
|
|
328
|
+
return MODEL_MAP.get(id);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if a model ID belongs to KiloCode Gateway
|
|
333
|
+
*/
|
|
334
|
+
export function isKiloModel(id: string): boolean {
|
|
335
|
+
return KILO_MODEL_IDS.has(id);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Determine the upstream provider for a given model ID
|
|
340
|
+
*/
|
|
341
|
+
export function getModelUpstream(id: string): Upstream {
|
|
342
|
+
return isKiloModel(id) ? "kilo" : "opencode";
|
|
343
|
+
}
|