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";...@@ -14,6 +14,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson";
14import {14import {
15 query as claudeQuery,15 query as claudeQuery,
16 type SlashCommand as ClaudeSlashCommand,16 type SlashCommand as ClaudeSlashCommand,
17 type SDKUserMessage,
17} from "@anthropic-ai/claude-agent-sdk";18} from "@anthropic-ai/claude-agent-sdk";
1819
19import {20import {
...@@ -480,13 +481,24 @@ function dedupeSlashCommands(...@@ -480,13 +481,24 @@ function dedupeSlashCommands(
480 return [...commandsByName.values()];481 return [...commandsByName.values()];
481}482}
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
483/**493/**
484 * Probe account information by spawning a lightweight Claude Agent SDK494 * Probe account information by spawning a lightweight Claude Agent SDK
485 * session and reading the initialization result.495 * session and reading the initialization result.
486 *496 *
487 * The prompt is never sent to the Anthropic API — we abort immediately497 * We pass a never-yielding AsyncIterable as the prompt so that no user
488 * after the local initialization phase completes. This gives us the498 * message is ever written to the subprocess stdin. This means the Claude
489 * user's subscription type without incurring any token cost.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.
490 *502 *
491 * This is used as a fallback when `claude auth status` does not include503 * This is used as a fallback when `claude auth status` does not include
492 * subscription type information.504 * subscription type information.
...@@ -495,12 +507,16 @@ const probeClaudeCapabilities = (binaryPath: string) => {...@@ -495,12 +507,16 @@ const probeClaudeCapabilities = (binaryPath: string) => {
495 const abort = new AbortController();507 const abort = new AbortController();
496 return Effect.tryPromise(async () => {508 return Effect.tryPromise(async () => {
497 const q = claudeQuery({509 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 })(),
499 options: {516 options: {
500 persistSession: false,517 persistSession: false,
501 pathToClaudeCodeExecutable: binaryPath,518 pathToClaudeCodeExecutable: binaryPath,
502 abortController: abort,519 abortController: abort,
503 maxTurns: 0,
504 settingSources: ["user", "project", "local"],520 settingSources: ["user", "project", "local"],
505 allowedTools: [],521 allowedTools: [],
506 stderr: () => {},522 stderr: () => {},