fix(server): better copilot error handle (#10509)

This commit is contained in:
darkskygit
2025-02-28 11:01:58 +00:00
parent 7227b7f8f6
commit 6b76037e39
2 changed files with 81 additions and 49 deletions

View File

@@ -37,6 +37,8 @@ import {
Config, Config,
CopilotFailedToGenerateText, CopilotFailedToGenerateText,
CopilotSessionNotFound, CopilotSessionNotFound,
InternalServerError,
mapAnyError,
mapSseError, mapSseError,
metrics, metrics,
NoCopilotProviderAvailable, NoCopilotProviderAvailable,
@@ -148,8 +150,8 @@ export class CopilotController implements BeforeApplicationShutdown {
if (!messageId || retry) { if (!messageId || retry) {
// revert the latest message generated by the assistant // revert the latest message generated by the assistant
// if messageId is provided, we will also revert latest user message // if messageId is provided, we will also revert latest user message
await this.chatSession.revertLatestMessage(sessionId, !messageId); await this.chatSession.revertLatestMessage(sessionId, !!messageId);
session.revertLatestMessage(!messageId); session.revertLatestMessage(!!messageId);
} }
if (messageId) { if (messageId) {
@@ -209,31 +211,34 @@ export class CopilotController implements BeforeApplicationShutdown {
@Param('sessionId') sessionId: string, @Param('sessionId') sessionId: string,
@Query() params: Record<string, string | string[]> @Query() params: Record<string, string | string[]>
): Promise<string> { ): Promise<string> {
const { messageId, retry } = this.prepareParams(params); const info: any = { sessionId, params };
const provider = await this.chooseTextProvider(
user.id,
sessionId,
messageId
);
const session = await this.appendSessionMessage(
sessionId,
messageId,
retry
);
try { try {
metrics.ai.counter('chat_calls').add(1, { model: session.model }); const { messageId, retry } = this.prepareParams(params);
const content = await provider.generateText(
session.finish(params), const provider = await this.chooseTextProvider(
session.model, user.id,
{ sessionId,
...session.config.promptConfig, messageId
signal: this.getSignal(req),
user: user.id,
}
); );
const session = await this.appendSessionMessage(
sessionId,
messageId,
retry
);
info.model = session.model;
metrics.ai.counter('chat_calls').add(1, { model: session.model });
const finalMessage = session.finish(params);
info.finalMessage = finalMessage;
const content = await provider.generateText(finalMessage, session.model, {
...session.config.promptConfig,
signal: this.getSignal(req),
user: user.id,
});
session.push({ session.push({
role: 'assistant', role: 'assistant',
content, content,
@@ -243,8 +248,13 @@ export class CopilotController implements BeforeApplicationShutdown {
return content; return content;
} catch (e: any) { } catch (e: any) {
metrics.ai.counter('chat_errors').add(1, { model: session.model }); metrics.ai.counter('chat_errors').add(1);
throw new CopilotFailedToGenerateText(e.message); let error = mapAnyError(e);
if (error instanceof InternalServerError) {
error = new CopilotFailedToGenerateText(e.message);
}
error.log('CopilotChat', info);
throw error;
} }
} }
@@ -276,8 +286,10 @@ export class CopilotController implements BeforeApplicationShutdown {
metrics.ai.counter('chat_stream_calls').add(1, { model: session.model }); metrics.ai.counter('chat_stream_calls').add(1, { model: session.model });
this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1);
const finalMessage = session.finish(params);
info.finalMessage = finalMessage;
const source$ = from( const source$ = from(
provider.generateTextStream(session.finish(params), session.model, { provider.generateTextStream(finalMessage, session.model, {
...session.config.promptConfig, ...session.config.promptConfig,
signal: this.getSignal(req), signal: this.getSignal(req),
user: user.id, user: user.id,

View File

@@ -21,15 +21,24 @@ export type PerplexityConfig = {
endpoint?: string; endpoint?: string;
}; };
const PerplexityErrorSchema = z.object({ const PerplexityErrorSchema = z.union([
detail: z.array( z.object({
z.object({ detail: z.array(
loc: z.array(z.string()), z.object({
msg: z.string(), loc: z.array(z.string()),
msg: z.string(),
type: z.string(),
})
),
}),
z.object({
error: z.object({
message: z.string(),
type: z.string(), type: z.string(),
}) code: z.number(),
), }),
}); }),
]);
const PerplexityDataSchema = z.object({ const PerplexityDataSchema = z.object({
citations: z.array(z.string()), citations: z.array(z.string()),
@@ -50,6 +59,8 @@ const PerplexityDataSchema = z.object({
const PerplexitySchema = z.union([PerplexityDataSchema, PerplexityErrorSchema]); const PerplexitySchema = z.union([PerplexityDataSchema, PerplexityErrorSchema]);
type PerplexityError = z.infer<typeof PerplexityErrorSchema>;
export class CitationParser { export class CitationParser {
private readonly SQUARE_BRACKET_OPEN = '['; private readonly SQUARE_BRACKET_OPEN = '[';
@@ -214,12 +225,8 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
params params
); );
const data = PerplexitySchema.parse(await response.json()); const data = PerplexitySchema.parse(await response.json());
if ('detail' in data) { if ('detail' in data || 'error' in data) {
throw new CopilotProviderSideError({ throw this.convertError(data);
provider: this.type,
kind: 'unexpected_response',
message: data.detail[0].msg || 'Unexpected perplexity response',
});
} else { } else {
const citationParser = new CitationParser(); const citationParser = new CitationParser();
const { content } = data.choices[0].message; const { content } = data.choices[0].message;
@@ -264,9 +271,9 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
this.config.endpoint || 'https://api.perplexity.ai/chat/completions', this.config.endpoint || 'https://api.perplexity.ai/chat/completions',
params params
); );
if (response.body) { const errorHandler = this.convertError;
if (response.ok && response.body) {
const citationParser = new CitationParser(); const citationParser = new CitationParser();
const provider = this.type;
const eventStream = response.body const eventStream = response.body
.pipeThrough(new TextDecoderStream()) .pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream()) .pipeThrough(new EventSourceParserStream())
@@ -280,13 +287,8 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
const json = JSON.parse(chunk.data); const json = JSON.parse(chunk.data);
if (json) { if (json) {
const data = PerplexitySchema.parse(json); const data = PerplexitySchema.parse(json);
if ('detail' in data) { if ('detail' in data || 'error' in data) {
throw new CopilotProviderSideError({ throw errorHandler(data);
provider,
kind: 'unexpected_response',
message:
data.detail[0].msg || 'Unexpected perplexity response',
});
} }
const { content } = data.choices[0].delta; const { content } = data.choices[0].delta;
const { citations } = data; const { citations } = data;
@@ -331,6 +333,24 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
} }
} }
private convertError(e: PerplexityError) {
function getErrMessage(e: PerplexityError) {
let err = 'Unexpected perplexity response';
if ('detail' in e) {
err = e.detail[0].msg || err;
} else if ('error' in e) {
err = e.error.message || err;
}
return err;
}
throw new CopilotProviderSideError({
provider: this.type,
kind: 'unexpected_response',
message: getErrMessage(e),
});
}
private handleError(e: any) { private handleError(e: any) {
if (e instanceof CopilotProviderSideError) { if (e instanceof CopilotProviderSideError) {
return e; return e;