fix(ai-builder): Long running prompts throw internal 401 error rather than expected complexity error (#24114)

This commit is contained in:
Michael Drury
2026-01-13 13:27:40 +00:00
committed by GitHub
parent 8eb5e756d2
commit 71ca931188
2 changed files with 75 additions and 5 deletions
@@ -197,6 +197,43 @@ describe('WorkflowBuilderAgent', () => {
}).rejects.toThrow(ApplicationError);
});
it('should handle 401 expired token error from LangChain MODEL_AUTHENTICATION', async () => {
// This matches the actual error structure from LangChain when the AI assistant proxy token expires
const expiredTokenError = Object.assign(new Error('Expired token'), {
status: 401,
lc_error_code: 'MODEL_AUTHENTICATION',
});
mockCreateStreamProcessor.mockImplementation(() => {
// eslint-disable-next-line require-yield
return (async function* () {
throw expiredTokenError;
})();
});
await expect(async () => {
const generator = agent.chat(mockPayload);
await generator.next();
}).rejects.toThrow(ApplicationError);
});
it('should not treat generic 401 errors as expired token errors', async () => {
// A generic 401 without lc_error_code should be rethrown as-is, not converted to ApplicationError
const generic401Error = Object.assign(new Error('Unauthorized'), { status: 401 });
mockCreateStreamProcessor.mockImplementation(() => {
// eslint-disable-next-line require-yield
return (async function* () {
throw generic401Error;
})();
});
await expect(async () => {
const generator = agent.chat(mockPayload);
await generator.next();
}).rejects.toThrow(generic401Error);
});
it('should handle invalid request errors', async () => {
const invalidRequestError = Object.assign(new Error('Request failed'), {
error: {
@@ -207,12 +244,18 @@ describe('WorkflowBuilderAgent', () => {
},
});
(mockLlmSimple.invoke as jest.Mock).mockRejectedValue(invalidRequestError);
// Mock the stream processor to throw the invalid request error
mockCreateStreamProcessor.mockImplementation(() => {
// eslint-disable-next-line require-yield
return (async function* () {
throw invalidRequestError;
})();
});
await expect(async () => {
const generator = agent.chat(mockPayload);
await generator.next();
}).rejects.toThrow(ApplicationError);
}).rejects.toThrow(ValidationError);
});
it('should rethrow unknown errors', async () => {
@@ -27,6 +27,9 @@ import type { WorkflowState } from './workflow-state';
const PROMPT_IS_TOO_LARGE_ERROR =
'The current conversation and workflow state is too large to process. Try to simplify your workflow by breaking it into smaller parts.';
const WORKFLOW_TOO_COMPLEX_ERROR =
'Workflow generation stopped: The AI reached the maximum number of steps while building your workflow. This usually means the workflow design became too complex or got stuck in a loop while trying to create the nodes and connections.';
/**
* Type for the state snapshot with properly typed values
*/
@@ -283,15 +286,39 @@ export class WorkflowBuilderAgent {
// If it's not an abort error, check for GraphRecursionError
if (error instanceof GraphRecursionError) {
throw new ApplicationError(
'Workflow generation stopped: The AI reached the maximum number of steps while building your workflow. This usually means the workflow design became too complex or got stuck in a loop while trying to create the nodes and connections.',
);
throw new ApplicationError(WORKFLOW_TOO_COMPLEX_ERROR);
}
// Check for 401 expired token errors (typically from long-running generations)
if (this.isTokenExpiredError(error)) {
throw new ApplicationError(WORKFLOW_TOO_COMPLEX_ERROR);
}
// Re-throw any other errors
throw error;
}
/**
* Checks if the error is a 401 expired token error from the LLM provider proxy.
* This typically occurs during very long-running workflow generations when
* the AI assistant service proxy token expires.
*
* We specifically check for LangChain's MODEL_AUTHENTICATION error code to ensure
* we only catch authentication errors from the LLM provider, not unrelated 401s.
*/
private isTokenExpiredError(error: unknown): boolean {
const LC_MODEL_AUTHENTICATION_ERROR = 'MODEL_AUTHENTICATION';
return (
!!error &&
typeof error === 'object' &&
'lc_error_code' in error &&
error.lc_error_code === LC_MODEL_AUTHENTICATION_ERROR &&
'status' in error &&
error.status === 401
);
}
private getInvalidRequestError(error: unknown): string | undefined {
if (
error instanceof Error &&