fix(share-ai): cap per-request output + unify provider errors (#60, #95)

#60: streamText had no maxOutputTokens, so one anonymous request could run up
the provider bill. Add maxOutputTokens (env SHARE_AI_MAX_OUTPUT_TOKENS, default
512) via resolveShareAiMaxOutputTokens().
#95: the anonymous path hand-built error strings, diverging from the unified
describeProviderError format used on the authenticated path; both onError blocks
now call describeProviderError so a share reader sees 402/429/503 causes in the
same form (and the stack is still logged). Both changes are in this one file and
share hunks, hence one commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 03:08:34 +03:00
parent 5344a9bdde
commit 05a7a4001f

View File

@@ -19,6 +19,7 @@ import {
PublicShareWorkspaceLimiter, PublicShareWorkspaceLimiter,
createPublicShareWorkspaceLimiter, createPublicShareWorkspaceLimiter,
} from './public-share-workspace-limiter'; } from './public-share-workspace-limiter';
import { describeProviderError } from '../../integrations/ai/ai-error.util';
/** /**
* Loose shape of the anonymous public-share chat POST body. We do NOT bind a * Loose shape of the anonymous public-share chat POST body. We do NOT bind a
@@ -63,6 +64,24 @@ export interface PublicShareChatStreamArgs {
export const MAX_SHARE_MESSAGES = 30; export const MAX_SHARE_MESSAGES = 30;
export const MAX_SHARE_MESSAGE_CHARS = 8000; export const MAX_SHARE_MESSAGE_CHARS = 8000;
/**
* Default per-request output cap for the anonymous share assistant. Bounds the
* tokens a single anonymous request can generate; worst case = steps x this.
*/
export const SHARE_AI_MAX_OUTPUT_TOKENS = 512;
/**
* Read the per-request output cap from the environment (overridable seam),
* falling back to the sane default. A non-positive / unparseable value uses the
* default. Mirrors resolveShareAiWorkspaceMax().
*/
export function resolveShareAiMaxOutputTokens(): number {
const raw = Number(process.env.SHARE_AI_MAX_OUTPUT_TOKENS);
return Number.isFinite(raw) && raw > 0
? Math.floor(raw)
: SHARE_AI_MAX_OUTPUT_TOKENS;
}
/** /**
* Keep ONLY genuine conversation turns from the client-held transcript. The * Keep ONLY genuine conversation turns from the client-held transcript. The
* payload is fully attacker-controlled; a forged `system` turn could try to * payload is fully attacker-controlled; a forged `system` turn could try to
@@ -204,16 +223,15 @@ export class PublicShareChatService {
tools, tools,
// Bound the agent loop for anonymous callers. // Bound the agent loop for anonymous callers.
stopWhen: stepCountIs(5), stopWhen: stepCountIs(5),
// Bounds per-request output so one anonymous request can't run up the
// provider bill; worst case = steps x this.
maxOutputTokens: resolveShareAiMaxOutputTokens(),
abortSignal: signal, abortSignal: signal,
onError: ({ error }) => { onError: ({ error }) => {
const e = error as { // Reuse the shared formatter so provider error formatting stays
statusCode?: number; // unified (statusCode + body) with the authenticated path.
message?: string; const e = error as { stack?: string };
stack?: string; const errorText = describeProviderError(error, String(error));
};
const errorText = e?.statusCode
? `${e.statusCode}: ${e.message ?? String(error)}`
: (e?.message ?? String(error));
// Never persist anonymous transcripts; just log the failure. // Never persist anonymous transcripts; just log the failure.
this.logger.error( this.logger.error(
`Public share chat stream error: ${errorText}`, `Public share chat stream error: ${errorText}`,
@@ -228,10 +246,11 @@ export class PublicShareChatService {
result.pipeUIMessageStreamToResponse(res.raw, { result.pipeUIMessageStreamToResponse(res.raw, {
headers: { 'X-Accel-Buffering': 'no' }, headers: { 'X-Accel-Buffering': 'no' },
onError: (error: unknown) => { onError: (error: unknown) => {
const e = error as { statusCode?: number; message?: string }; // Reuse the shared formatter so provider error formatting stays
return e?.statusCode // unified between the log line and the streamed error message — a
? `${e.statusCode}: ${e.message}` // share reader sees 402/429/503 causes consistently with the
: (e?.message ?? 'AI stream error'); // authenticated path.
return describeProviderError(error, 'AI stream error');
}, },
}); });