@smithers-orchestrator/agents 0.25.0 → 0.25.2
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/package.json +4 -4
- package/src/PiAgent.js +4 -0
- package/src/diagnostics/getDiagnosticStrategy.js +150 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/agents",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "AI SDK and CLI agent adapters for Smithers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"ai": "^6.0.168",
|
|
67
67
|
"effect": "^3.21.1",
|
|
68
68
|
"zod": "^4.3.6",
|
|
69
|
-
"@smithers-orchestrator/
|
|
70
|
-
"@smithers-orchestrator/
|
|
71
|
-
"@smithers-orchestrator/observability": "0.25.
|
|
69
|
+
"@smithers-orchestrator/errors": "0.25.2",
|
|
70
|
+
"@smithers-orchestrator/driver": "0.25.2",
|
|
71
|
+
"@smithers-orchestrator/observability": "0.25.2"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@types/bun": "latest",
|
package/src/PiAgent.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
/** @typedef {import("./DiagnosticCheck.ts").DiagnosticCheck} DiagnosticCheck */
|
|
3
6
|
/** @typedef {import("./DiagnosticCheckId.ts").DiagnosticCheckId} DiagnosticCheckId */
|
|
4
7
|
/** @typedef {import("./DiagnosticContext.ts").DiagnosticContext} DiagnosticContext */
|
|
@@ -201,24 +204,110 @@ function openaiModelsUrl(env) {
|
|
|
201
204
|
const base = (env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
202
205
|
return `${base}/models`;
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Resolve the Codex CLI config directory the same way the `codex` binary does:
|
|
209
|
+
* an explicit `CODEX_HOME` wins, otherwise `~/.codex` (honoring `$HOME`).
|
|
210
|
+
* @param {Record<string, string | undefined>} env
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function resolveCodexHome(env) {
|
|
214
|
+
const explicit = env.CODEX_HOME?.trim();
|
|
215
|
+
if (explicit) {
|
|
216
|
+
return explicit;
|
|
217
|
+
}
|
|
218
|
+
return join(env.HOME?.trim() || homedir(), ".codex");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* @typedef {{ apiKey: string; keySource: string } | { subscription: true } | { missing: true }} OpenAiCredentials
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* Read OpenAI credentials from `<CODEX_HOME>/auth.json`, the file `codex login`
|
|
225
|
+
* writes. Mirrors the codex binary's own auth resolution: a stored API key, or
|
|
226
|
+
* ChatGPT subscription tokens. Returns null when no usable credentials are
|
|
227
|
+
* present (file missing, unreadable, malformed, or empty).
|
|
228
|
+
* @param {Record<string, string | undefined>} env
|
|
229
|
+
* @returns {{ kind: "apiKey"; apiKey: string } | { kind: "subscription" } | null}
|
|
230
|
+
*/
|
|
231
|
+
function readCodexCliAuth(env) {
|
|
232
|
+
try {
|
|
233
|
+
const raw = readFileSync(join(resolveCodexHome(env), "auth.json"), "utf8");
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
if (typeof parsed?.OPENAI_API_KEY === "string" && parsed.OPENAI_API_KEY.trim()) {
|
|
236
|
+
return { kind: "apiKey", apiKey: parsed.OPENAI_API_KEY.trim() };
|
|
237
|
+
}
|
|
238
|
+
if (typeof parsed?.tokens?.access_token === "string" && parsed.tokens.access_token.trim()) {
|
|
239
|
+
return { kind: "subscription" };
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Resolve the OpenAI credentials a codex/pi invocation will actually use. An env
|
|
249
|
+
* `OPENAI_API_KEY` always wins. When it is absent and `codexCliAuth` is set, fall
|
|
250
|
+
* back to `<CODEX_HOME>/auth.json` (subscription tokens or a stored API key) the
|
|
251
|
+
* same way the codex binary does (#448). pi leaves `codexCliAuth` off — it reads
|
|
252
|
+
* the env var (or `--api-key`), not codex's auth.json.
|
|
253
|
+
* @param {Record<string, string | undefined>} env
|
|
254
|
+
* @param {boolean} codexCliAuth
|
|
255
|
+
* @returns {OpenAiCredentials}
|
|
256
|
+
*/
|
|
257
|
+
function resolveOpenAiCredentials(env, codexCliAuth) {
|
|
258
|
+
const envKey = env.OPENAI_API_KEY;
|
|
259
|
+
if (envKey) {
|
|
260
|
+
return { apiKey: envKey, keySource: "OPENAI_API_KEY" };
|
|
261
|
+
}
|
|
262
|
+
if (codexCliAuth) {
|
|
263
|
+
const auth = readCodexCliAuth(env);
|
|
264
|
+
if (auth?.kind === "apiKey") {
|
|
265
|
+
return { apiKey: auth.apiKey, keySource: "Codex CLI auth.json API key" };
|
|
266
|
+
}
|
|
267
|
+
if (auth?.kind === "subscription") {
|
|
268
|
+
return { subscription: true };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { missing: true };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* OpenAI API-key validity check via GET /v1/models (free, no tokens).
|
|
275
|
+
*
|
|
276
|
+
* Validates whatever concrete key the invocation will use — env `OPENAI_API_KEY`
|
|
277
|
+
* or, when `codexCliAuth` is set, a key stored in `<CODEX_HOME>/auth.json`. A
|
|
278
|
+
* stored key can be invalid/exhausted just like an env key, so it is probed, not
|
|
279
|
+
* trusted on presence. ChatGPT subscription tokens can't be probed cheaply, so
|
|
280
|
+
* (like the Claude subscription check) their presence passes (#448).
|
|
281
|
+
* @param {{ codexCliAuth: boolean }} options
|
|
282
|
+
* @returns {DiagnosticCheckDef}
|
|
283
|
+
*/
|
|
284
|
+
function openaiApiKeyCheck({ codexCliAuth }) {
|
|
285
|
+
return {
|
|
207
286
|
id: "api_key_valid",
|
|
208
287
|
run: async (ctx) => {
|
|
209
288
|
const start = performance.now();
|
|
210
|
-
const
|
|
211
|
-
if (
|
|
289
|
+
const creds = resolveOpenAiCredentials(ctx.env, codexCliAuth);
|
|
290
|
+
if ("subscription" in creds) {
|
|
291
|
+
return {
|
|
292
|
+
id: "api_key_valid",
|
|
293
|
+
status: "pass",
|
|
294
|
+
message: "No OPENAI_API_KEY set — using Codex CLI subscription auth (CODEX_HOME/auth.json)",
|
|
295
|
+
durationMs: performance.now() - start,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if ("missing" in creds) {
|
|
212
299
|
return {
|
|
213
300
|
id: "api_key_valid",
|
|
214
301
|
status: "fail",
|
|
215
|
-
message:
|
|
302
|
+
message: codexCliAuth
|
|
303
|
+
? "OPENAI_API_KEY not set and no Codex CLI auth found — run `codex login` or set OPENAI_API_KEY"
|
|
304
|
+
: "OPENAI_API_KEY not set",
|
|
216
305
|
durationMs: performance.now() - start,
|
|
217
306
|
};
|
|
218
307
|
}
|
|
219
308
|
try {
|
|
220
309
|
const res = await fetch(openaiModelsUrl(ctx.env), {
|
|
221
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
310
|
+
headers: { Authorization: `Bearer ${creds.apiKey}` },
|
|
222
311
|
signal: AbortSignal.timeout(4_000),
|
|
223
312
|
});
|
|
224
313
|
const elapsed = performance.now() - start;
|
|
@@ -226,7 +315,7 @@ const codexApiKeyAndRateLimitCheck = [
|
|
|
226
315
|
return {
|
|
227
316
|
id: "api_key_valid",
|
|
228
317
|
status: "fail",
|
|
229
|
-
message:
|
|
318
|
+
message: `${creds.keySource} is invalid (401 Unauthorized)`,
|
|
230
319
|
durationMs: elapsed,
|
|
231
320
|
};
|
|
232
321
|
}
|
|
@@ -234,14 +323,14 @@ const codexApiKeyAndRateLimitCheck = [
|
|
|
234
323
|
return {
|
|
235
324
|
id: "api_key_valid",
|
|
236
325
|
status: "fail",
|
|
237
|
-
message:
|
|
326
|
+
message: `${creds.keySource} lacks permission (403 Forbidden)`,
|
|
238
327
|
durationMs: elapsed,
|
|
239
328
|
};
|
|
240
329
|
}
|
|
241
330
|
return {
|
|
242
331
|
id: "api_key_valid",
|
|
243
332
|
status: "pass",
|
|
244
|
-
message:
|
|
333
|
+
message: `${creds.keySource} is valid`,
|
|
245
334
|
durationMs: elapsed,
|
|
246
335
|
};
|
|
247
336
|
}
|
|
@@ -254,23 +343,34 @@ const codexApiKeyAndRateLimitCheck = [
|
|
|
254
343
|
};
|
|
255
344
|
}
|
|
256
345
|
},
|
|
257
|
-
}
|
|
258
|
-
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Rate-limit probe via GET /v1/models (free, no tokens). Probes the same key the
|
|
350
|
+
* api-key check resolves (env or Codex CLI auth.json), so a stored key's quota is
|
|
351
|
+
* checked too; subscription/no-key resolve to a non-blocking skip.
|
|
352
|
+
* @param {{ codexCliAuth: boolean }} options
|
|
353
|
+
* @returns {DiagnosticCheckDef}
|
|
354
|
+
*/
|
|
355
|
+
function openaiRateLimitCheck({ codexCliAuth }) {
|
|
356
|
+
return {
|
|
259
357
|
id: "rate_limit_status",
|
|
260
358
|
run: async (ctx) => {
|
|
261
359
|
const start = performance.now();
|
|
262
|
-
const
|
|
263
|
-
if (!apiKey) {
|
|
360
|
+
const creds = resolveOpenAiCredentials(ctx.env, codexCliAuth);
|
|
361
|
+
if (!("apiKey" in creds)) {
|
|
264
362
|
return {
|
|
265
363
|
id: "rate_limit_status",
|
|
266
364
|
status: "skip",
|
|
267
|
-
message: "
|
|
365
|
+
message: "subscription" in creds
|
|
366
|
+
? "Subscription mode — cannot probe rate limits via API"
|
|
367
|
+
: "No API key — cannot check rate limits",
|
|
268
368
|
durationMs: 0,
|
|
269
369
|
};
|
|
270
370
|
}
|
|
271
371
|
try {
|
|
272
372
|
const res = await fetch(openaiModelsUrl(ctx.env), {
|
|
273
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
373
|
+
headers: { Authorization: `Bearer ${creds.apiKey}` },
|
|
274
374
|
signal: AbortSignal.timeout(4_000),
|
|
275
375
|
});
|
|
276
376
|
const elapsed = performance.now() - start;
|
|
@@ -324,7 +424,13 @@ const codexApiKeyAndRateLimitCheck = [
|
|
|
324
424
|
};
|
|
325
425
|
}
|
|
326
426
|
},
|
|
327
|
-
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
// Codex resolves auth from `<CODEX_HOME>/auth.json` (subscription tokens or a
|
|
430
|
+
// stored API key) when OPENAI_API_KEY is absent, so its checks honor that.
|
|
431
|
+
const codexApiKeyAndRateLimitCheck = [
|
|
432
|
+
openaiApiKeyCheck({ codexCliAuth: true }),
|
|
433
|
+
openaiRateLimitCheck({ codexCliAuth: true }),
|
|
328
434
|
];
|
|
329
435
|
const codexStrategy = {
|
|
330
436
|
agentId: "codex",
|
|
@@ -475,12 +581,11 @@ const antigravityStrategy = {
|
|
|
475
581
|
],
|
|
476
582
|
};
|
|
477
583
|
// ---------------------------------------------------------------------------
|
|
478
|
-
// Pi strategy
|
|
584
|
+
// Pi strategy helpers — dispatch checks based on which provider pi is using
|
|
479
585
|
// ---------------------------------------------------------------------------
|
|
480
586
|
/**
|
|
481
587
|
* Resolve the effective pi provider family from an explicit `--provider`, a
|
|
482
|
-
* `provider/model` prefix, or a bare model id's well-known prefix.
|
|
483
|
-
* when undeterminable so callers fall back to pi's default (google) (#284).
|
|
588
|
+
* `provider/model` prefix, or a bare model id's well-known prefix.
|
|
484
589
|
* @param {DiagnosticHints | undefined} hints
|
|
485
590
|
* @returns {string}
|
|
486
591
|
*/
|
|
@@ -516,12 +621,31 @@ function resolvePiProvider(hints) {
|
|
|
516
621
|
function piProviderChecks(hints) {
|
|
517
622
|
const raw = resolvePiProvider(hints);
|
|
518
623
|
if (raw === "openai" || raw === "openai-codex" || raw === "azure" || raw === "azure-openai") {
|
|
519
|
-
|
|
624
|
+
// pi reads OPENAI_API_KEY from the env (or --api-key), not codex's
|
|
625
|
+
// auth.json, so it still requires the key — no Codex CLI auth fallback.
|
|
626
|
+
return [
|
|
627
|
+
openaiApiKeyCheck({ codexCliAuth: false }),
|
|
628
|
+
openaiRateLimitCheck({ codexCliAuth: false }),
|
|
629
|
+
];
|
|
520
630
|
}
|
|
521
631
|
if (raw === "anthropic" || raw === "claude") {
|
|
522
632
|
return [claudeApiKeyCheck, claudeRateLimitCheck];
|
|
523
633
|
}
|
|
524
|
-
|
|
634
|
+
if (raw === "google" || raw === "gemini") {
|
|
635
|
+
return [googleAuthCheck, googleRateLimitCheck];
|
|
636
|
+
}
|
|
637
|
+
// Unknown provider — skip preflight, pi handles its own auth
|
|
638
|
+
return [
|
|
639
|
+
{
|
|
640
|
+
id: "api_key_valid",
|
|
641
|
+
run: async () => ({
|
|
642
|
+
id: "api_key_valid",
|
|
643
|
+
status: "skip",
|
|
644
|
+
message: `Pi provider "${raw || "unset"}" — passing auth to pi`,
|
|
645
|
+
durationMs: 0,
|
|
646
|
+
}),
|
|
647
|
+
},
|
|
648
|
+
];
|
|
525
649
|
}
|
|
526
650
|
/**
|
|
527
651
|
* pi accepts credentials via the `--api-key` option instead of an environment
|
|
@@ -544,7 +668,10 @@ export function diagnosticApiKeyEnv(command, hints) {
|
|
|
544
668
|
if (raw === "anthropic" || raw === "claude") {
|
|
545
669
|
return { ANTHROPIC_API_KEY: hints.apiKey };
|
|
546
670
|
}
|
|
547
|
-
|
|
671
|
+
if (raw === "google" || raw === "gemini") {
|
|
672
|
+
return { GOOGLE_API_KEY: hints.apiKey };
|
|
673
|
+
}
|
|
674
|
+
return undefined;
|
|
548
675
|
}
|
|
549
676
|
// ---------------------------------------------------------------------------
|
|
550
677
|
// Amp strategy
|