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,
createPublicShareWorkspaceLimiter,
} 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
@@ -63,6 +64,24 @@ export interface PublicShareChatStreamArgs {
export const MAX_SHARE_MESSAGES = 30;
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
* payload is fully attacker-controlled; a forged `system` turn could try to
@@ -204,16 +223,15 @@ export class PublicShareChatService {
tools,
// Bound the agent loop for anonymous callers.
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,
onError: ({ error }) => {
const e = error as {
statusCode?: number;
message?: string;
stack?: string;
};
const errorText = e?.statusCode
? `${e.statusCode}: ${e.message ?? String(error)}`
: (e?.message ?? String(error));
// Reuse the shared formatter so provider error formatting stays
// unified (statusCode + body) with the authenticated path.
const e = error as { stack?: string };
const errorText = describeProviderError(error, String(error));
// Never persist anonymous transcripts; just log the failure.
this.logger.error(
`Public share chat stream error: ${errorText}`,
@@ -228,10 +246,11 @@ export class PublicShareChatService {
result.pipeUIMessageStreamToResponse(res.raw, {
headers: { 'X-Accel-Buffering': 'no' },
onError: (error: unknown) => {
const e = error as { statusCode?: number; message?: string };
return e?.statusCode
? `${e.statusCode}: ${e.message}`
: (e?.message ?? 'AI stream error');
// Reuse the shared formatter so provider error formatting stays
// unified between the log line and the streamed error message — a
// share reader sees 402/429/503 causes consistently with the
// authenticated path.
return describeProviderError(error, 'AI stream error');
},
});