@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/agents",
3
- "version": "0.25.0",
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/driver": "0.25.0",
70
- "@smithers-orchestrator/errors": "0.25.0",
71
- "@smithers-orchestrator/observability": "0.25.0"
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
@@ -510,6 +510,10 @@ export class PiAgent extends BaseCliAgent {
510
510
  options: params.options,
511
511
  mode,
512
512
  }),
513
+ hints: {
514
+ provider: this.opts.provider,
515
+ model: this.opts.model ?? this.model,
516
+ },
513
517
  outputFormat: mode,
514
518
  };
515
519
  }
@@ -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
- // Combined API key validation + rate limit check via GET /v1/models (free, no tokens)
205
- const codexApiKeyAndRateLimitCheck = [
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 apiKey = ctx.env.OPENAI_API_KEY;
211
- if (!apiKey) {
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: "OPENAI_API_KEY not set",
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: "OPENAI_API_KEY is invalid (401 Unauthorized)",
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: "OPENAI_API_KEY lacks permission (403 Forbidden)",
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: "OPENAI_API_KEY is valid",
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 apiKey = ctx.env.OPENAI_API_KEY;
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: "No API key — cannot check rate limits",
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. Returns ""
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
- return [...codexApiKeyAndRateLimitCheck];
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
- return [googleAuthCheck, googleRateLimitCheck];
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
- return { GOOGLE_API_KEY: hints.apiKey };
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