Skip to content

Commit

Permalink
Add a new VertexAI error type
Browse files Browse the repository at this point in the history
  • Loading branch information
dlarocque committed May 9, 2024
1 parent ab883d0 commit 54977c3
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 131 deletions.
35 changes: 25 additions & 10 deletions packages/vertexai/src/api.test.ts
Expand Up @@ -19,7 +19,7 @@ import { getGenerativeModel } from './api';
import { expect } from 'chai';
import { VertexAI } from './public-types';
import { GenerativeModel } from './models/generative-model';
import { VertexError } from './errors';
import { VertexAIError, VertexAIErrorCode } from './errors';

const fakeVertexAI: VertexAI = {
app: {
Expand All @@ -35,27 +35,42 @@ const fakeVertexAI: VertexAI = {

describe('Top level API', () => {
it('getGenerativeModel throws if no model is provided', () => {
expect(() => getGenerativeModel(fakeVertexAI, {} as ModelParams)).to.throw(
VertexError.NO_MODEL
);
try {
getGenerativeModel(fakeVertexAI, {} as ModelParams);
} catch (e) {
expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_MODEL);
expect((e as VertexAIError).message).equals('Missing model parameter');
}
});
it('getGenerativeModel throws if no apiKey is provided', () => {
const fakeVertexNoApiKey = {
...fakeVertexAI,
app: { options: { projectId: 'my-project' } }
} as VertexAI;
expect(() =>
getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' })
).to.throw(VertexError.NO_API_KEY);
try {
getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' });
} catch (e) {
expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_API_KEY);
expect((e as VertexAIError).message).equals(
'Missing Firebase app API key'
);
}
});
it('getGenerativeModel throws if no projectId is provided', () => {
const fakeVertexNoProject = {
...fakeVertexAI,
app: { options: { apiKey: 'my-key' } }
} as VertexAI;
expect(() =>
getGenerativeModel(fakeVertexNoProject, { model: 'my-model' })
).to.throw(VertexError.NO_PROJECT_ID);
try {
getGenerativeModel(fakeVertexNoProject, { model: 'my-model' });
} catch (e) {
expect((e as VertexAIError).code).includes(
VertexAIErrorCode.NO_PROJECT_ID
);
expect((e as VertexAIError).message).equals(
'Missing Firebase app project ID'
);
}
});
it('getGenerativeModel gets a GenerativeModel', () => {
const genModel = getGenerativeModel(fakeVertexAI, { model: 'my-model' });
Expand Down
7 changes: 5 additions & 2 deletions packages/vertexai/src/api.ts
Expand Up @@ -21,9 +21,9 @@ import { getModularInstance } from '@firebase/util';
import { DEFAULT_LOCATION, VERTEX_TYPE } from './constants';
import { VertexAIService } from './service';
import { VertexAI, VertexAIOptions } from './public-types';
import { ERROR_FACTORY, VertexError } from './errors';
import { ModelParams, RequestOptions } from './types';
import { GenerativeModel } from './models/generative-model';
import { VertexAIError, VertexAIErrorCode } from './errors';

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

Expand Down Expand Up @@ -67,7 +67,10 @@ export function getGenerativeModel(
requestOptions?: RequestOptions
): GenerativeModel {
if (!modelParams.model) {
throw ERROR_FACTORY.create(VertexError.NO_MODEL);
throw new VertexAIError(
VertexAIErrorCode.NO_MODEL,
'Missing model parameter'
);
}
return new GenerativeModel(vertexAI, modelParams, requestOptions);
}
117 changes: 81 additions & 36 deletions packages/vertexai/src/errors.ts
Expand Up @@ -14,50 +14,95 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FirebaseError } from '@firebase/util';

import { ErrorFactory, ErrorMap } from '@firebase/util';
import { GenerateContentResponse } from './types';
/**
* Standardized error codes that {@link VertexAIError} can have.
*
* @public
*/
export const enum VertexAIErrorCode {
/** A generic error occured. */
ERROR = 'error',

export const enum VertexError {
/** An error occurred in a request */
REQUEST_ERROR = 'request-error',

/** An error occured in a response. */
RESPONSE_ERROR = 'response-error',

/** An error occurred while performing a fetch */
FETCH_ERROR = 'fetch-error',

/** An error associated with a Content object. */
INVALID_CONTENT = 'invalid-content',

/** An error occured due to a missing api key */
NO_API_KEY = 'no-api-key',

/** An error occurred due to a missing model */
NO_MODEL = 'no-model',

/** An error occured due to a missing project id */
NO_PROJECT_ID = 'no-project-id',
PARSE_FAILED = 'parse-failed',
RESPONSE_ERROR = 'response-error'

/** An error occured while parsing */
PARSE_FAILED = 'parse-failed'
}

const ERRORS: ErrorMap<VertexError> = {
[VertexError.FETCH_ERROR]: `Error fetching from {$url}: {$message}`,
[VertexError.INVALID_CONTENT]: `Content formatting error: {$message}`,
[VertexError.NO_API_KEY]:
`The "apiKey" field is empty in the local Firebase config. Firebase VertexAI requires this field to` +
`contain a valid API key.`,
[VertexError.NO_PROJECT_ID]:
`The "projectId" field is empty in the local Firebase config. Firebase VertexAI requires this field to` +
`contain a valid project ID.`,
[VertexError.NO_MODEL]:
`Must provide a model name. ` +
`Example: getGenerativeModel({ model: 'my-model-name' })`,
[VertexError.PARSE_FAILED]: `Parsing failed: {$message}`,
[VertexError.RESPONSE_ERROR]:
`Response error: {$message}. Response body stored in ` +
`error.customData.response`
};

interface ErrorParams {
[VertexError.FETCH_ERROR]: { url: string; message: string };
[VertexError.INVALID_CONTENT]: { message: string };
[VertexError.PARSE_FAILED]: { message: string };
[VertexError.RESPONSE_ERROR]: {
message: string;
response: GenerateContentResponse;
};
/**
* Details object that may be included in an error response.
*
* @public
*/
interface ErrorDetails {
'@type'?: string;

/** The reason for the error */
reason?: string;

/** The domain where the error occured. */
domain?: string;

/** Additonal metadata about the error. */
metadata?: Record<string, unknown>;

/** Any other relevant information about the error. */
[key: string]: unknown;
}

export const ERROR_FACTORY = new ErrorFactory<VertexError, ErrorParams>(
'vertexAI',
'VertexAI',
ERRORS
);
/**
* Error class for the Firebase VertexAI SDK.
*
* @public
*/
export class VertexAIError extends FirebaseError {
/**
* Stack trace of the error.
*/
readonly stack?: string;

/**
* Creates a new VertexAIError instance.
*
* @param code - The error code from {@link VertexAIErrorCode}.
* @param message - A human-readable message describing the error.
* @param status - Optional HTTP status code of the error response.
* @param statusText - Optional HTTP status text of the error response.
* @param errorDetails - Optional additional details about the error.
*/
constructor(
readonly code: VertexAIErrorCode,
readonly message: string,
readonly status?: number,
readonly statusText?: string,
readonly errorDetails?: ErrorDetails[]
) {
// Match error format used by FirebaseError from ErrorFactory
const service = 'vertex-ai';
const serviceName = 'VertexAI';
const fullCode = `${service}/${code}`;
const fullMessage = `${serviceName}: ${message} (${fullCode})`;
super(fullCode, fullMessage);
}
}
44 changes: 25 additions & 19 deletions packages/vertexai/src/methods/chat-session-helpers.ts
Expand Up @@ -16,7 +16,7 @@
*/

import { Content, POSSIBLE_ROLES, Part, Role } from '../types';
import { ERROR_FACTORY, VertexError } from '../errors';
import { VertexAIError, VertexAIErrorCode } from '../errors';

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

Expand Down Expand Up @@ -48,28 +48,32 @@ export function validateChatHistory(history: Content[]): void {
for (const currContent of history) {
const { role, parts } = currContent;
if (!prevContent && role !== 'user') {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: `First content should be with role 'user', got ${role}`
});
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
`First content should be with role 'user', got ${role}`
);
}
if (!POSSIBLE_ROLES.includes(role)) {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
`Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify(
POSSIBLE_ROLES
)}`
});
);
}

if (!Array.isArray(parts)) {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: "Content should have 'parts' property with an array of Parts"
});
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
`Content should have 'parts' but property with an array of Parts`
);
}

if (parts.length === 0) {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: 'Each Content should have at least one part'
});
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
`Each content should have at least one part`
);
}

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

if (prevContent) {
const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role];
if (!validPreviousContentRoles.includes(prevContent.role)) {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: `Content with role '${role}' can't follow '${
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
`Content with role '${role} can't follow '${
prevContent.role
}'. Valid previous roles: ${JSON.stringify(
VALID_PREVIOUS_CONTENT_ROLES
)}`
});
);
}
}
prevContent = currContent;
Expand Down
12 changes: 9 additions & 3 deletions packages/vertexai/src/models/generative-model.ts
Expand Up @@ -42,7 +42,7 @@ import {
formatSystemInstruction
} from '../requests/request-helpers';
import { VertexAI } from '../public-types';
import { ERROR_FACTORY, VertexError } from '../errors';
import { VertexAIError, VertexAIErrorCode } from '../errors';
import { ApiSettings } from '../types/internal';
import { VertexAIService } from '../service';

Expand All @@ -66,9 +66,15 @@ export class GenerativeModel {
requestOptions?: RequestOptions
) {
if (!vertexAI.app?.options?.apiKey) {
throw ERROR_FACTORY.create(VertexError.NO_API_KEY);
throw new VertexAIError(
VertexAIErrorCode.NO_API_KEY,
'Missing Firebase app API key'
);
} else if (!vertexAI.app?.options?.projectId) {
throw ERROR_FACTORY.create(VertexError.NO_PROJECT_ID);
throw new VertexAIError(
VertexAIErrorCode.NO_PROJECT_ID,
'Missing Firebase app project ID'
);
} else {
this._apiSettings = {
apiKey: vertexAI.app.options.apiKey,
Expand Down
17 changes: 9 additions & 8 deletions packages/vertexai/src/requests/request-helpers.ts
Expand Up @@ -16,7 +16,7 @@
*/

import { Content, GenerateContentRequest, Part } from '../types';
import { ERROR_FACTORY, VertexError } from '../errors';
import { VertexAIError, VertexAIErrorCode } from '../errors';

export function formatSystemInstruction(
input?: string | Part | Content
Expand Down Expand Up @@ -81,16 +81,17 @@ function assignRoleToPartsAndValidateSendMessageRequest(
}

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

if (!hasUserContent && !hasFunctionContent) {
throw ERROR_FACTORY.create(VertexError.INVALID_CONTENT, {
message: 'No content is provided for sending chat message.'
});
throw new VertexAIError(
VertexAIErrorCode.INVALID_CONTENT,
'No content is provided for sending chat message.'
);
}

if (hasUserContent) {
Expand Down

0 comments on commit 54977c3

Please sign in to comment.