authorgravatar for 7143787+reasv@users.noreply.github.comreasv <7143787+reasv@users.noreply.github.com> 2026-04-20 02:13:20+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-20 00:13:20+00:00
log8dbcf92a0d125050988474f258df3e55c538efec
tree5e21c0a8f4a37a441f4273b31b8a8e4d0d3abace
parent37965da01d34e50a67f92cd7fb978b74a6cf8fcf
signature Signed by PGP key B5690EEEBB952194

fix(server): prevent probeClaudeCapabilities from wasting API requests (#2192)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

1 files changed, 21 insertions(+), 5 deletions(-)

apps/server/src/provider/Layers/ClaudeProvider.ts+21-5
......@@ -14,6 +14,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson";
1414import {
1515 query as claudeQuery,
1616 type SlashCommand as ClaudeSlashCommand,
17 type SDKUserMessage,
1718} from "@anthropic-ai/claude-agent-sdk";
1819
1920import {
......@@ -480,13 +481,24 @@ function dedupeSlashCommands(
480481 return [...commandsByName.values()];
481482}
482483
484function waitForAbortSignal(signal: AbortSignal): Promise<void> {
485 if (signal.aborted) {
486 return Promise.resolve();
487 }
488 return new Promise((resolve) => {
489 signal.addEventListener("abort", () => resolve(), { once: true });
490 });
491}
492
483493/**
484494 * Probe account information by spawning a lightweight Claude Agent SDK
485495 * session and reading the initialization result.
486496 *
487 * The prompt is never sent to the Anthropic API — we abort immediately
488 * after the local initialization phase completes. This gives us the
489 * user's subscription type without incurring any token cost.
497 * We pass a never-yielding AsyncIterable as the prompt so that no user
498 * message is ever written to the subprocess stdin. This means the Claude
499 * Code subprocess completes its local initialization IPC (returning
500 * account info and slash commands) but never starts an API request to
501 * Anthropic. We read the init data and then abort the subprocess.
490502 *
491503 * This is used as a fallback when `claude auth status` does not include
492504 * subscription type information.
......@@ -495,12 +507,16 @@ const probeClaudeCapabilities = (binaryPath: string) => {
495507 const abort = new AbortController();
496508 return Effect.tryPromise(async () => {
497509 const q = claudeQuery({
498 prompt: ".",
510 // Never yield — we only need initialization data, not a conversation.
511 // This prevents any prompt from reaching the Anthropic API.
512 // oxlint-disable-next-line require-yield
513 prompt: (async function* (): AsyncGenerator<SDKUserMessage> {
514 await waitForAbortSignal(abort.signal);
515 })(),
499516 options: {
500517 persistSession: false,
501518 pathToClaudeCodeExecutable: binaryPath,
502519 abortController: abort,
503 maxTurns: 0,
504520 settingSources: ["user", "project", "local"],
505521 allowedTools: [],
506522 stderr: () => {},