@almightygpt/core 0.10.0 → 0.11.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/dist/adapters/defaults.d.ts.map +1 -1
- package/dist/adapters/defaults.js +5 -0
- package/dist/adapters/defaults.js.map +1 -1
- package/dist/adapters/factory.d.ts.map +1 -1
- package/dist/adapters/factory.js +11 -1
- package/dist/adapters/factory.js.map +1 -1
- package/dist/adapters/index.d.ts +9 -5
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +9 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/ollama.d.ts +47 -0
- package/dist/adapters/ollama.d.ts.map +1 -0
- package/dist/adapters/ollama.js +124 -0
- package/dist/adapters/ollama.js.map +1 -0
- package/dist/adapters/openrouter.d.ts +50 -0
- package/dist/adapters/openrouter.d.ts.map +1 -0
- package/dist/adapters/openrouter.js +148 -0
- package/dist/adapters/openrouter.js.map +1 -0
- package/dist/auth/__tests__/keychain.test.d.ts +18 -0
- package/dist/auth/__tests__/keychain.test.d.ts.map +1 -0
- package/dist/auth/__tests__/keychain.test.js +155 -0
- package/dist/auth/__tests__/keychain.test.js.map +1 -0
- package/dist/auth/__tests__/resolver.test.d.ts +13 -0
- package/dist/auth/__tests__/resolver.test.d.ts.map +1 -0
- package/dist/auth/__tests__/resolver.test.js +200 -0
- package/dist/auth/__tests__/resolver.test.js.map +1 -0
- package/dist/auth/__tests__/validator.test.d.ts +15 -0
- package/dist/auth/__tests__/validator.test.d.ts.map +1 -0
- package/dist/auth/__tests__/validator.test.js +197 -0
- package/dist/auth/__tests__/validator.test.js.map +1 -0
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js +8 -0
- package/dist/auth/types.js.map +1 -1
- package/dist/auth/validator.d.ts.map +1 -1
- package/dist/auth/validator.js +117 -11
- package/dist/auth/validator.js.map +1 -1
- package/dist/config/schema.d.ts +12 -12
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +8 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +4 -2
- package/src/adapters/defaults.ts +5 -0
- package/src/adapters/factory.ts +11 -1
- package/src/adapters/index.ts +9 -5
- package/src/adapters/ollama.ts +157 -0
- package/src/adapters/openrouter.ts +194 -0
- package/src/auth/__tests__/keychain.test.ts +171 -0
- package/src/auth/__tests__/resolver.test.ts +255 -0
- package/src/auth/__tests__/validator.test.ts +241 -0
- package/src/auth/types.ts +15 -1
- package/src/auth/validator.ts +130 -11
- package/src/config/schema.ts +8 -1
- package/src/index.ts +1 -1
package/src/auth/validator.ts
CHANGED
|
@@ -72,6 +72,20 @@ export async function validateKey(
|
|
|
72
72
|
case "google":
|
|
73
73
|
result = await validateGoogle(key);
|
|
74
74
|
break;
|
|
75
|
+
case "openrouter":
|
|
76
|
+
result = await validateOpenRouter(key);
|
|
77
|
+
break;
|
|
78
|
+
case "ollama":
|
|
79
|
+
// Ollama has no key — "validation" means probing the local
|
|
80
|
+
// daemon. The key arg is ignored.
|
|
81
|
+
result = await validateOllama();
|
|
82
|
+
break;
|
|
83
|
+
default: {
|
|
84
|
+
// Exhaustiveness check — TypeScript will error here if a new
|
|
85
|
+
// ProviderId is added without a switch case.
|
|
86
|
+
const _exhaustive: never = provider;
|
|
87
|
+
throw new Error(`Unknown provider: ${String(_exhaustive)}`);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
result.latencyMs = Date.now() - start;
|
|
77
91
|
return result;
|
|
@@ -107,7 +121,7 @@ async function validateOpenAI(key: string): Promise<ValidationResult> {
|
|
|
107
121
|
return {
|
|
108
122
|
ok: false,
|
|
109
123
|
statusCode: res.status,
|
|
110
|
-
error: normalizeOpenAIError(res.status, rawBody),
|
|
124
|
+
error: normalizeOpenAIError(res.status, rawBody, key),
|
|
111
125
|
rawBody,
|
|
112
126
|
};
|
|
113
127
|
}
|
|
@@ -142,7 +156,7 @@ async function validateAnthropic(key: string): Promise<ValidationResult> {
|
|
|
142
156
|
return {
|
|
143
157
|
ok: false,
|
|
144
158
|
statusCode: res.status,
|
|
145
|
-
error: normalizeAnthropicError(res.status, rawBody),
|
|
159
|
+
error: normalizeAnthropicError(res.status, rawBody, key),
|
|
146
160
|
rawBody,
|
|
147
161
|
};
|
|
148
162
|
}
|
|
@@ -187,34 +201,112 @@ async function validateGoogle(key: string): Promise<ValidationResult> {
|
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
203
|
|
|
204
|
+
async function validateOpenRouter(key: string): Promise<ValidationResult> {
|
|
205
|
+
// OpenRouter is OpenAI-compatible at https://openrouter.ai/api/v1.
|
|
206
|
+
// Same chat-completions shape — point fetch at OR's base URL.
|
|
207
|
+
const model = DEFAULT_MODELS.openrouter;
|
|
208
|
+
const controller = new AbortController();
|
|
209
|
+
const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
"content-type": "application/json",
|
|
215
|
+
authorization: `Bearer ${key}`,
|
|
216
|
+
"HTTP-Referer": "https://github.com/roxjayanath/almightygpt",
|
|
217
|
+
"X-Title": "AlmightyGPT validation",
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
model,
|
|
221
|
+
messages: [{ role: "user", content: "hi" }],
|
|
222
|
+
max_tokens: 1,
|
|
223
|
+
}),
|
|
224
|
+
signal: controller.signal,
|
|
225
|
+
});
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
const rawBody = await res.text().catch(() => "");
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
statusCode: res.status,
|
|
231
|
+
error: normalizeOpenRouterError(res.status, rawBody, key),
|
|
232
|
+
rawBody,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const data = (await res.json()) as { model?: string };
|
|
236
|
+
return { ok: true, model: data.model ?? model };
|
|
237
|
+
} finally {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function validateOllama(): Promise<ValidationResult> {
|
|
243
|
+
// No key. Probe the local daemon at /api/tags — cheap, no model
|
|
244
|
+
// invocation. If the user has Ollama running, this confirms the
|
|
245
|
+
// adapter can reach it. Doesn't confirm the default model is
|
|
246
|
+
// pulled — that surfaces on the first real call.
|
|
247
|
+
const baseUrl = process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434";
|
|
248
|
+
const probeUrl = baseUrl.replace(/\/v1\/?$/, "") + "/api/tags";
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
251
|
+
try {
|
|
252
|
+
const res = await fetch(probeUrl, { signal: controller.signal });
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
statusCode: res.status,
|
|
257
|
+
error: `[${res.status}] Ollama daemon at ${baseUrl} responded with an error.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
return { ok: true, model: DEFAULT_MODELS.ollama };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error:
|
|
265
|
+
`Ollama daemon unreachable at ${baseUrl}. Is Ollama installed ` +
|
|
266
|
+
`and running? Try \`ollama serve\` or open the Ollama app.`,
|
|
267
|
+
rawBody: err instanceof Error ? err.message : String(err),
|
|
268
|
+
};
|
|
269
|
+
} finally {
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
190
274
|
// ─── Error normalization (Codex v0.8 P2 #6) ──────────────────────────
|
|
191
275
|
//
|
|
192
276
|
// Parse known provider JSON error shapes into short, user-safe messages.
|
|
193
277
|
// Never echo the raw key back even by accident (defense in depth: we
|
|
194
278
|
// also redact anything that looks like the submitted key).
|
|
195
279
|
|
|
196
|
-
function normalizeOpenAIError(
|
|
280
|
+
function normalizeOpenAIError(
|
|
281
|
+
status: number,
|
|
282
|
+
rawBody: string,
|
|
283
|
+
submittedKey: string,
|
|
284
|
+
): string {
|
|
197
285
|
// OpenAI shape: { "error": { "message": "...", "type": "...", "code": "..." } }
|
|
198
286
|
try {
|
|
199
287
|
const parsed = JSON.parse(rawBody) as {
|
|
200
288
|
error?: { message?: string; type?: string; code?: string };
|
|
201
289
|
};
|
|
202
290
|
const msg = parsed.error?.message;
|
|
203
|
-
if (msg) return `[${status}] OpenAI: ${truncate(msg, 200)}`;
|
|
291
|
+
if (msg) return `[${status}] OpenAI: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
204
292
|
} catch {
|
|
205
293
|
/* fall through */
|
|
206
294
|
}
|
|
207
295
|
return statusOnlyMessage("OpenAI", status);
|
|
208
296
|
}
|
|
209
297
|
|
|
210
|
-
function normalizeAnthropicError(
|
|
298
|
+
function normalizeAnthropicError(
|
|
299
|
+
status: number,
|
|
300
|
+
rawBody: string,
|
|
301
|
+
submittedKey: string,
|
|
302
|
+
): string {
|
|
211
303
|
// Anthropic shape: { "type": "error", "error": { "type": "...", "message": "..." } }
|
|
212
304
|
try {
|
|
213
305
|
const parsed = JSON.parse(rawBody) as {
|
|
214
306
|
error?: { type?: string; message?: string };
|
|
215
307
|
};
|
|
216
308
|
const msg = parsed.error?.message;
|
|
217
|
-
if (msg) return `[${status}] Anthropic: ${truncate(msg, 200)}`;
|
|
309
|
+
if (msg) return `[${status}] Anthropic: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
218
310
|
} catch {
|
|
219
311
|
/* fall through */
|
|
220
312
|
}
|
|
@@ -231,18 +323,45 @@ function normalizeGoogleError(
|
|
|
231
323
|
const parsed = JSON.parse(rawBody) as {
|
|
232
324
|
error?: { code?: number; message?: string; status?: string };
|
|
233
325
|
};
|
|
326
|
+
const msg = parsed.error?.message ?? "";
|
|
327
|
+
if (msg) return `[${status}] Google: ${truncate(redactKey(msg, submittedKey), 200)}`;
|
|
328
|
+
} catch {
|
|
329
|
+
/* fall through */
|
|
330
|
+
}
|
|
331
|
+
return statusOnlyMessage("Google", status);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Belt-and-braces: if a provider echoes the submitted key in its
|
|
336
|
+
* error body, redact before surfacing to the user. Codex's v0.8 P2 #6
|
|
337
|
+
* found this gap (originally Google-only); now applied to all three
|
|
338
|
+
* providers via this shared helper.
|
|
339
|
+
*/
|
|
340
|
+
function redactKey(msg: string, key: string): string {
|
|
341
|
+
if (!key || !msg.includes(key)) return msg;
|
|
342
|
+
return msg.split(key).join("<redacted-key>");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeOpenRouterError(
|
|
346
|
+
status: number,
|
|
347
|
+
rawBody: string,
|
|
348
|
+
submittedKey: string,
|
|
349
|
+
): string {
|
|
350
|
+
// OpenRouter forwards provider errors but wraps them; shape is
|
|
351
|
+
// typically { "error": { "message": "...", "code": N } }.
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(rawBody) as {
|
|
354
|
+
error?: { message?: string; code?: number };
|
|
355
|
+
};
|
|
234
356
|
let msg = parsed.error?.message ?? "";
|
|
235
|
-
// Belt-and-braces redaction: Google sometimes echoes the key in
|
|
236
|
-
// error messages (e.g. "API key not valid. Pass a valid API key.")
|
|
237
|
-
// — we don't ship the actual key value if it ever ends up here.
|
|
238
357
|
if (submittedKey && msg.includes(submittedKey)) {
|
|
239
358
|
msg = msg.replace(submittedKey, "<redacted-key>");
|
|
240
359
|
}
|
|
241
|
-
if (msg) return `[${status}]
|
|
360
|
+
if (msg) return `[${status}] OpenRouter: ${truncate(msg, 200)}`;
|
|
242
361
|
} catch {
|
|
243
362
|
/* fall through */
|
|
244
363
|
}
|
|
245
|
-
return statusOnlyMessage("
|
|
364
|
+
return statusOnlyMessage("OpenRouter", status);
|
|
246
365
|
}
|
|
247
366
|
|
|
248
367
|
function statusOnlyMessage(provider: string, status: number): string {
|
package/src/config/schema.ts
CHANGED
|
@@ -13,7 +13,14 @@ export const AgentRoleSchema = z.enum(["worker", "reviewer", "both", "optional"]
|
|
|
13
13
|
export const AgentConfigSchema = z
|
|
14
14
|
.object({
|
|
15
15
|
enabled: z.boolean().default(true),
|
|
16
|
-
provider: z.enum([
|
|
16
|
+
provider: z.enum([
|
|
17
|
+
"openai",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"google",
|
|
20
|
+
"openrouter",
|
|
21
|
+
"ollama",
|
|
22
|
+
"mock",
|
|
23
|
+
]),
|
|
17
24
|
mode: z.enum(["api", "cli"]).default("api"),
|
|
18
25
|
role: AgentRoleSchema.default("optional"),
|
|
19
26
|
memoryFile: z.string().min(1),
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - budget/ ✅ task #14 BudgetTracker + BudgetExceededError
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
export const VERSION = "0.
|
|
16
|
+
export const VERSION = "0.11.0";
|
|
17
17
|
|
|
18
18
|
// MCP server (v0.9.0+) — exposes AlmightyGPT's review surface as MCP tools.
|
|
19
19
|
export { startMcpServer } from "./mcp/server.js";
|