Skip to content

Commit 54977c3

Browse files
committed
Add a new VertexAI error type
1 parent ab883d0 commit 54977c3

File tree

10 files changed

+257
-131
lines changed

10 files changed

+257
-131
lines changed

packages/vertexai/src/api.test.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { getGenerativeModel } from './api';
1919
import { expect } from 'chai';
2020
import { VertexAI } from './public-types';
2121
import { GenerativeModel } from './models/generative-model';
22-
import { VertexError } from './errors';
22+
import { VertexAIError, VertexAIErrorCode } from './errors';
2323

2424
const fakeVertexAI: VertexAI = {
2525
app: {
@@ -35,27 +35,42 @@ const fakeVertexAI: VertexAI = {
3535

3636
describe('Top level API', () => {
3737
it('getGenerativeModel throws if no model is provided', () => {
38-
expect(() => getGenerativeModel(fakeVertexAI, {} as ModelParams)).to.throw(
39-
VertexError.NO_MODEL
40-
);
38+
try {
39+
getGenerativeModel(fakeVertexAI, {} as ModelParams);
40+
} catch (e) {
41+
expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_MODEL);
42+
expect((e as VertexAIError).message).equals('Missing model parameter');
43+
}
4144
});
4245
it('getGenerativeModel throws if no apiKey is provided', () => {
4346
const fakeVertexNoApiKey = {
4447
...fakeVertexAI,
4548
app: { options: { projectId: 'my-project' } }
4649
} as VertexAI;
47-
expect(() =>
48-
getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' })
49-
).to.throw(VertexError.NO_API_KEY);
50+
try {
51+
getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' });
52+
} catch (e) {
53+
expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_API_KEY);
54+
expect((e as VertexAIError).message).equals(
55+
'Missing Firebase app API key'
56+
);
57+
}
5058
});
5159
it('getGenerativeModel throws if no projectId is provided', () => {
5260
const fakeVertexNoProject = {
5361
...fakeVertexAI,
5462
app: { options: { apiKey: 'my-key' } }
5563
} as VertexAI;
56-
expect(() =>
57-
getGenerativeModel(fakeVertexNoProject, { model: 'my-model' })
58-
).to.throw(VertexError.NO_PROJECT_ID);
64+
try {
65+
getGenerativeModel(fakeVertexNoProject, { model: 'my-model' });
66+
} catch (e) {
67+
expect((e as VertexAIError).code).includes(
68+
VertexAIErrorCode.NO_PROJECT_ID
69+
);
70+
expect((e as VertexAIError).message).equals(
71+
'Missing Firebase app project ID'
72+
);
73+
}
5974
});
6075
it('getGenerativeModel gets a GenerativeModel', () => {
6176
const genModel = getGenerativeModel(fakeVertexAI, { model: 'my-model' });

packages/vertexai/src/api.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import { getModularInstance } from '@firebase/util';
2121
import { DEFAULT_LOCATION, VERTEX_TYPE } from './constants';
2222
import { VertexAIService } from './service';
2323
import { VertexAI, VertexAIOptions } from './public-types';
24-
import { ERROR_FACTORY, VertexError } from './errors';
2524
import { ModelParams, RequestOptions } from './types';
2625
import { GenerativeModel } from './models/generative-model';
26+
import { VertexAIError, VertexAIErrorCode } from './errors';
2727

2828
export { ChatSession } from './methods/chat-session';
2929

@@ -67,7 +67,10 @@ export function getGenerativeModel(
6767
requestOptions?: RequestOptions
6868
): GenerativeModel {
6969
if (!modelParams.model) {
70-
throw ERROR_FACTORY.create(VertexError.NO_MODEL);
70+
throw new VertexAIError(
71+
VertexAIErrorCode.NO_MODEL,
72+
'Missing model parameter'
73+
);
7174
}
7275
return new GenerativeModel(vertexAI, modelParams, requestOptions);
7376
}

packages/vertexai/src/errors.ts

Lines changed: 81 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,50 +14,95 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
import { FirebaseError } from '@firebase/util';
1718

18-
import { ErrorFactory, ErrorMap } from '@firebase/util';
19-
import { GenerateContentResponse } from './types';
19+
/**
20+
* Standardized error codes that {@link VertexAIError} can have.
21+
*
22+
* @public
23+
*/
24+
export const enum VertexAIErrorCode {
25+
/** A generic error occured. */
26+
ERROR = 'error',
2027

21-
export const enum VertexError {
28+
/** An error occurred in a request */
29+
REQUEST_ERROR = 'request-error',
30+
31+
/** An error occured in a response. */
32+
RESPONSE_ERROR = 'response-error',
33+
34+
/** An error occurred while performing a fetch */
2235
FETCH_ERROR = 'fetch-error',
36+
37+
/** An error associated with a Content object. */
2338
INVALID_CONTENT = 'invalid-content',
39+
40+
/** An error occured due to a missing api key */
2441
NO_API_KEY = 'no-api-key',
42+
43+
/** An error occurred due to a missing model */
2544
NO_MODEL = 'no-model',
45+
46+
/** An error occured due to a missing project id */
2647
NO_PROJECT_ID = 'no-project-id',
27-
PARSE_FAILED = 'parse-failed',
28-
RESPONSE_ERROR = 'response-error'
48+
49+
/** An error occured while parsing */
50+
PARSE_FAILED = 'parse-failed'
2951
}
3052

31-
const ERRORS: ErrorMap<VertexError> = {
32-
[VertexError.FETCH_ERROR]: `Error fetching from {$url}: {$message}`,
33-
[VertexError.INVALID_CONTENT]: `Content formatting error: {$message}`,
34-
[VertexError.NO_API_KEY]:
35-
`The "apiKey" field is empty in the local Firebase config. Firebase VertexAI requires this field to` +
36-
`contain a valid API key.`,
37-
[VertexError.NO_PROJECT_ID]:
38-
`The "projectId" field is empty in the local Firebase config. Firebase VertexAI requires this field to` +
39-
`contain a valid project ID.`,
40-
[VertexError.NO_MODEL]:
41-
`Must provide a model name. ` +
42-
`Example: getGenerativeModel({ model: 'my-model-name' })`,
43-
[VertexError.PARSE_FAILED]: `Parsing failed: {$message}`,
44-
[VertexError.RESPONSE_ERROR]:
45-
`Response error: {$message}. Response body stored in ` +
46-
`error.customData.response`
47-
};
48-
49-
interface ErrorParams {
50-
[VertexError.FETCH_ERROR]: { url: string; message: string };
51-
[VertexError.INVALID_CONTENT]: { message: string };
52-
[VertexError.PARSE_FAILED]: { message: string };
53-
[VertexError.RESPONSE_ERROR]: {
54-
message: string;
55-
response: GenerateContentResponse;
56-
};
53+
/**
54+
* Details object that may be included in an error response.
55+
*
56+
* @public
57+
*/
58+
interface ErrorDetails {
59+
'@type'?: string;
60+
61+
/** The reason for the error */
62+
reason?: string;
63+
64+
/** The domain where the error occured. */
65+
domain?: string;
66+
67+
/** Additonal metadata about the error. */
68+
metadata?: Record<string, unknown>;
69+
70+
/** Any other relevant information about the error. */
71+
[key: string]: unknown;
5772
}
5873

59-
export const ERROR_FACTORY = new ErrorFactory<VertexError, ErrorParams>(
60-
'vertexAI',
61-
'VertexAI',
62-
ERRORS
63-
);
74+
/**
75+
* Error class for the Firebase VertexAI SDK.
76+
*
77+
* @public
78+
*/
79+
export class VertexAIError extends FirebaseError {
80+
/**
81+
* Stack trace of the error.
82+
*/
83+
readonly stack?: string;
84+
85+
/**
86+
* Creates a new VertexAIError instance.
87+
*
88+
* @param code - The error code from {@link VertexAIErrorCode}.
89+
* @param message - A human-readable message describing the error.
90+
* @param status - Optional HTTP status code of the error response.
91+
* @param statusText - Optional HTTP status text of the error response.
92+
* @param errorDetails - Optional additional details about the error.
93+
*/
94+
constructor(
95+
readonly code: VertexAIErrorCode,
96+
readonly message: string,
97+
readonly status?: number,
98+
readonly statusText?: string,
99+
readonly errorDetails?: ErrorDetails[]
100+
) {
101+
// Match error format used by FirebaseError from ErrorFactory
102+
const service = 'vertex-ai';
103+
const serviceName = 'VertexAI';
104+
const fullCode = `${service}/${code}`;
105+
const fullMessage = `${serviceName}: ${message} (${fullCode})`;
106+
super(fullCode, fullMessage);
107+
}
108+
}

packages/vertexai/src/methods/chat-session-helpers.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { Content, POSSIBLE_ROLES, Part, Role } from '../types';
19-
import { ERROR_FACTORY, VertexError } from '../errors';
19+
import { VertexAIError, VertexAIErrorCode } from '../errors';
2020

2121
// https://ai.google.dev/api/rest/v1beta/Content#part
2222

@@ -48,28 +48,32 @@ export function validateChatHistory(history: Content[]): void {
4848
for (const currContent of history) {
4949
const { role, parts } = currContent;
5050
if (!prevContent && role !== 'user') {
51-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
52-
message: `First content should be with role 'user', got ${role}`
53-
});
51+
throw new VertexAIError(
52+
VertexAIErrorCode.INVALID_CONTENT,
53+
`First content should be with role 'user', got ${role}`
54+
);
5455
}
5556
if (!POSSIBLE_ROLES.includes(role)) {
56-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
57-
message: `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(
57+
throw new VertexAIError(
58+
VertexAIErrorCode.INVALID_CONTENT,
59+
`Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(
5860
POSSIBLE_ROLES
5961
)}`
60-
});
62+
);
6163
}
6264

6365
if (!Array.isArray(parts)) {
64-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
65-
message: "Content should have 'parts' property with an array of Parts"
66-
});
66+
throw new VertexAIError(
67+
VertexAIErrorCode.INVALID_CONTENT,
68+
`Content should have 'parts' but property with an array of Parts`
69+
);
6770
}
6871

6972
if (parts.length === 0) {
70-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
71-
message: 'Each Content should have at least one part'
72-
});
73+
throw new VertexAIError(
74+
VertexAIErrorCode.INVALID_CONTENT,
75+
`Each content should have at least one part`
76+
);
7377
}
7478

7579
const countFields: Record<keyof Part, number> = {
@@ -89,22 +93,24 @@ export function validateChatHistory(history: Content[]): void {
8993
const validParts = VALID_PARTS_PER_ROLE[role];
9094
for (const key of VALID_PART_FIELDS) {
9195
if (!validParts.includes(key) && countFields[key] > 0) {
92-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
93-
message: `Content with role '${role}' can't contain '${key}' part`
94-
});
96+
throw new VertexAIError(
97+
VertexAIErrorCode.INVALID_CONTENT,
98+
`Content with role '${role}' can't contain '${key}' part`
99+
);
95100
}
96101
}
97102

98103
if (prevContent) {
99104
const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role];
100105
if (!validPreviousContentRoles.includes(prevContent.role)) {
101-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
102-
message: `Content with role '${role}' can't follow '${
106+
throw new VertexAIError(
107+
VertexAIErrorCode.INVALID_CONTENT,
108+
`Content with role '${role} can't follow '${
103109
prevContent.role
104110
}'. Valid previous roles: ${JSON.stringify(
105111
VALID_PREVIOUS_CONTENT_ROLES
106112
)}`
107-
});
113+
);
108114
}
109115
}
110116
prevContent = currContent;

packages/vertexai/src/models/generative-model.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import {
4242
formatSystemInstruction
4343
} from '../requests/request-helpers';
4444
import { VertexAI } from '../public-types';
45-
import { ERROR_FACTORY, VertexError } from '../errors';
45+
import { VertexAIError, VertexAIErrorCode } from '../errors';
4646
import { ApiSettings } from '../types/internal';
4747
import { VertexAIService } from '../service';
4848

@@ -66,9 +66,15 @@ export class GenerativeModel {
6666
requestOptions?: RequestOptions
6767
) {
6868
if (!vertexAI.app?.options?.apiKey) {
69-
throw ERROR_FACTORY.create(VertexError.NO_API_KEY);
69+
throw new VertexAIError(
70+
VertexAIErrorCode.NO_API_KEY,
71+
'Missing Firebase app API key'
72+
);
7073
} else if (!vertexAI.app?.options?.projectId) {
71-
throw ERROR_FACTORY.create(VertexError.NO_PROJECT_ID);
74+
throw new VertexAIError(
75+
VertexAIErrorCode.NO_PROJECT_ID,
76+
'Missing Firebase app project ID'
77+
);
7278
} else {
7379
this._apiSettings = {
7480
apiKey: vertexAI.app.options.apiKey,

packages/vertexai/src/requests/request-helpers.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717

1818
import { Content, GenerateContentRequest, Part } from '../types';
19-
import { ERROR_FACTORY, VertexError } from '../errors';
19+
import { VertexAIError, VertexAIErrorCode } from '../errors';
2020

2121
export function formatSystemInstruction(
2222
input?: string | Part | Content
@@ -81,16 +81,17 @@ function assignRoleToPartsAndValidateSendMessageRequest(
8181
}
8282

8383
if (hasUserContent && hasFunctionContent) {
84-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
85-
message:
86-
'Within a single message, FunctionResponse cannot be mixed with other type of part in the request for sending chat message.'
87-
});
84+
throw new VertexAIError(
85+
VertexAIErrorCode.INVALID_CONTENT,
86+
'Within a single message, FunctionResponse cannot be mixed with other type of part in the request for sending chat message.'
87+
);
8888
}
8989

9090
if (!hasUserContent && !hasFunctionContent) {
91-
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
92-
message: 'No content is provided for sending chat message.'
93-
});
91+
throw new VertexAIError(
92+
VertexAIErrorCode.INVALID_CONTENT,
93+
'No content is provided for sending chat message.'
94+
);
9495
}
9596

9697
if (hasUserContent) {

0 commit comments

Comments
 (0)