blob: 19e1bfabeaf83072a5338b0d2d7aa7fe84904a1e [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/ai/exception_helpers.h"
#include "base/debug/dump_without_crashing.h"
#include "base/notreached.h"
#include "third_party/blink/public/mojom/ai/ai_common.mojom-blink.h"
#include "third_party/blink/public/mojom/ai/ai_manager.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/ai/ai_manager.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_quota_exceeded_error_options.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/quota_exceeded_error.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/casting.h"
namespace blink {
const char kExceptionMessageExecutionContextInvalid[] =
"The execution context is not valid.";
const char kExceptionMessageServiceUnavailable[] =
"Model execution service is not available.";
const char kExceptionMessageDocumentNotActive[] = "The document is not active.";
const char kExceptionMessagePermissionDenied[] =
"A user permission error occurred, such as not signed-in or not "
"allowed to execute model.";
const char kExceptionMessageGenericError[] = "Other generic failures occurred.";
const char kExceptionMessageFiltered[] =
"The execution yielded an unsafe response.";
const char kExceptionMessageOutputLanguageFiltered[] =
"The model attempted to output text in an untested language, and was "
"prevented from doing so.";
const char kExceptionMessageResponseLowQuality[] =
"The model attempted to output text with low quality, and was prevented "
"from doing so.";
const char kExceptionMessageDisabled[] = "The response was disabled.";
const char kExceptionMessageCancelled[] = "The request was cancelled.";
const char kExceptionMessageSessionDestroyed[] =
"The model execution session has been destroyed.";
const char kExceptionMessageRequestAborted[] = "The request has been aborted.";
const char kExceptionMessageInputTooLarge[] = "The input is too large.";
const char kExceptionMessageInvalidTemperatureAndTopKFormat[] =
"Initializing a new session must either specify both topK and temperature, "
"or neither of them.";
const char kExceptionMessageInvalidTopK[] =
"The topK value provided is invalid.";
const char kExceptionMessageInvalidTemperature[] =
"The temperature value provided is invalid.";
const char kExceptionMessageUnableToCreateSession[] =
"The device is unable to create a session to run the model. "
"Please check the result of availability() first.";
const char kExceptionMessageUnableToCloneSession[] =
"The session cannot be cloned.";
const char kExceptionMessageUnableToCalculateUsage[] =
"The usage cannot be calculated.";
const char kExceptionMessagePromptWithSystemRoleIsNotTheFirst[] =
"The prompt with 'system' role must be placed at the first entry of "
"initialPrompts.";
const char kExceptionMessageUnsupportedLanguages[] =
"The specified languages are not supported.";
const char kExceptionMessageInvalidResponseJsonSchema[] =
"Response json schema is invalid - it should be an object that can be "
"stringified into a JSON string.";
const char kExceptionMessagePermissionPolicy[] =
"Access denied because the Permission Policy is not enabled.";
const char kExceptionMessageUserActivationRequired[] =
"Requires a user gesture when availability is \"downloading\" or "
"\"downloadable\".";
void ThrowInvalidContextException(ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kExceptionMessageExecutionContextInvalid);
}
void ThrowDocumentNotActiveException(ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kExceptionMessageDocumentNotActive);
}
void ThrowSessionDestroyedException(ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
kExceptionMessageSessionDestroyed);
}
void ThrowAbortedException(ExceptionState& exception_state) {
exception_state.ThrowDOMException(DOMExceptionCode::kAbortError,
kExceptionMessageRequestAborted);
}
void RejectPromiseWithInternalError(ScriptPromiseResolverBase* resolver) {
if (resolver) {
resolver->Reject(CreateInternalErrorException());
}
}
DOMException* CreateInternalErrorException() {
return DOMException::Create(
kExceptionMessageServiceUnavailable,
DOMException::GetErrorName(DOMExceptionCode::kOperationError));
}
DOMException* CreateSessionDestroyedException() {
return DOMException::Create(
kExceptionMessageSessionDestroyed,
DOMException::GetErrorName(DOMExceptionCode::kInvalidStateError));
}
bool HandleAbortSignal(AbortSignal* signal,
ScriptState* script_state,
ExceptionState& exception_state) {
if (signal && signal->aborted()) {
auto reason = signal->reason(script_state);
if (reason.IsEmpty()) {
ThrowAbortedException(exception_state);
} else {
V8ThrowException::ThrowException(script_state->GetIsolate(),
reason.V8Value());
}
return true;
}
return false;
}
bool ValidateScriptState(ScriptState* script_state,
ExceptionState& exception_state,
bool permit_workers) {
if (!script_state->ContextIsValid()) {
ThrowInvalidContextException(exception_state);
return false;
}
ExecutionContext* context = ExecutionContext::From(script_state);
if (context->IsServiceWorkerGlobalScope()) {
return permit_workers;
}
LocalDOMWindow* window = DynamicTo<LocalDOMWindow>(context);
// Realm’s global object must be a Window object.
CHECK(window);
// If document is not fully active, then return a promise rejected with an
// "InvalidStateError" DOMException.
Document* document = window->document();
CHECK(document);
if (!document->IsActive()) {
ThrowDocumentNotActiveException(exception_state);
return false;
}
return true;
}
String ValidateAndStringifyObject(const ScriptValue& input,
ScriptState* script_state,
ExceptionState& exception_state) {
v8::Local<v8::String> value;
if (!input.V8Value()->IsObject() ||
!v8::JSON::Stringify(script_state->GetContext(),
input.V8Value().As<v8::Object>())
.ToLocal(&value)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
kExceptionMessageInvalidResponseJsonSchema);
return String();
}
return ToBlinkString<String>(script_state->GetIsolate(), value,
kDoNotExternalize);
}
namespace {
// Create an UnknownError exception, include `error` in the exception
// message. This is intended for handling values of
// `ModelStreamingResponseStatus` that we do not expect to ever see when
// using an on-device model, e.g. errors related to servers.
DOMException* CreateUnknown(const char* error) {
return DOMException::Create(
StrCat({"An unknown error occurred: ", error}),
DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
}
} // namespace
DOMException* ConvertModelStreamingResponseErrorToDOMException(
ModelStreamingResponseStatus error,
mojom::blink::QuotaErrorInfoPtr quota_error_info) {
switch (error) {
case ModelStreamingResponseStatus::kErrorUnknown:
base::debug::DumpWithoutCrashing();
return CreateUnknown("kErrorUnknown");
case ModelStreamingResponseStatus::kErrorInvalidRequest:
base::debug::DumpWithoutCrashing();
return CreateUnknown("kErrorInvalidRequest");
case ModelStreamingResponseStatus::kErrorRequestThrottled:
base::debug::DumpWithoutCrashing();
return CreateUnknown("kErrorRequestThrottled");
case ModelStreamingResponseStatus::kErrorPermissionDenied:
return DOMException::Create(
kExceptionMessagePermissionDenied,
DOMException::GetErrorName(DOMExceptionCode::kNotAllowedError));
case ModelStreamingResponseStatus::kErrorGenericFailure:
return DOMException::Create(
kExceptionMessageGenericError,
DOMException::GetErrorName(DOMExceptionCode::kUnknownError));
case ModelStreamingResponseStatus::kErrorRetryableError:
base::debug::DumpWithoutCrashing();
return CreateUnknown("kErrorRetryableError");
case ModelStreamingResponseStatus::kErrorNonRetryableError:
base::debug::DumpWithoutCrashing();
return CreateUnknown("kErrorNonRetryableError");
case ModelStreamingResponseStatus::kErrorUnsupportedLanguage:
return DOMException::Create(
kExceptionMessageOutputLanguageFiltered,
DOMException::GetErrorName(DOMExceptionCode::kNotSupportedError));
case ModelStreamingResponseStatus::kErrorFiltered:
return DOMException::Create(
kExceptionMessageFiltered,
DOMException::GetErrorName(DOMExceptionCode::kNotReadableError));
case ModelStreamingResponseStatus::kErrorDisabled:
return DOMException::Create(
kExceptionMessageDisabled,
DOMException::GetErrorName(DOMExceptionCode::kAbortError));
case ModelStreamingResponseStatus::kErrorCancelled:
return DOMException::Create(
kExceptionMessageCancelled,
DOMException::GetErrorName(DOMExceptionCode::kAbortError));
case ModelStreamingResponseStatus::kErrorSessionDestroyed:
return DOMException::Create(
kExceptionMessageSessionDestroyed,
DOMException::GetErrorName(DOMExceptionCode::kInvalidStateError));
case ModelStreamingResponseStatus::kErrorInputTooLarge:
if (RuntimeEnabledFeatures::QuotaExceededErrorUpdateEnabled()) {
CHECK(quota_error_info);
auto* options = MakeGarbageCollected<QuotaExceededErrorOptions>();
options->setQuota(static_cast<double>(quota_error_info->quota));
options->setRequested(static_cast<double>(quota_error_info->requested));
return QuotaExceededError::Create(kExceptionMessageInputTooLarge,
std::move(options));
}
return DOMException::Create(
kExceptionMessageInputTooLarge,
DOMException::GetErrorName(DOMExceptionCode::kQuotaExceededError));
case ModelStreamingResponseStatus::kErrorResponseLowQuality:
return DOMException::Create(
kExceptionMessageResponseLowQuality,
DOMException::GetErrorName(DOMExceptionCode::kNotSupportedError));
case ModelStreamingResponseStatus::kOngoing:
case ModelStreamingResponseStatus::kComplete:
NOTREACHED();
}
NOTREACHED();
}
// LINT.IfChange(ConvertModelAvailabilityCheckResultToDebugString)
String ConvertModelAvailabilityCheckResultToDebugString(
mojom::blink::ModelAvailabilityCheckResult result) {
switch (result) {
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableServiceNotRunning:
return "Unable to create a text session because the service is not "
"running.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableUnsupportedLanguage:
return "The requested language options are not supported.";
case mojom::blink::ModelAvailabilityCheckResult::kUnavailableUnknown:
return "The service is unable to create new session.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableFeatureNotEnabled:
return "The feature flag gating model execution was disabled.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableConfigNotAvailableForFeature:
return "The model was available but there was not an execution config "
"available for the feature.";
case mojom::blink::ModelAvailabilityCheckResult::kUnavailableGpuBlocked:
return "The GPU is blocked.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableTooManyRecentCrashes:
return "The model process crashed too many times for this version.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableSafetyModelNotAvailable:
return "The safety model was required but not available.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableSafetyConfigNotAvailableForFeature:
return "The safety model was available but there was not a safety config "
"available for the feature.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableLanguageDetectionModelNotAvailable:
return "The language detection model was required but not available.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableFeatureExecutionNotEnabled:
return "Model execution for this feature was not enabled.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableModelAdaptationNotAvailable:
return "Model capability is not available.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableValidationPending:
return "Model validation is still pending.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableValidationFailed:
return "Model validation failed.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableModelNotEligible:
return "The device is not eligible for running on-device model.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableInsufficientDiskSpace:
return "The device does not have enough space for downloading the "
"on-device model";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableTranslationNotEligible:
return "The on-device translation is not available.";
case mojom::blink::ModelAvailabilityCheckResult::
kUnavailableEnterprisePolicyDisabled:
return "The on-device model is not available because the enterprise "
"policy disables the feature.";
case mojom::blink::ModelAvailabilityCheckResult::kAvailable:
case mojom::blink::ModelAvailabilityCheckResult::kDownloadable:
case mojom::blink::ModelAvailabilityCheckResult::kDownloading:
NOTREACHED();
}
NOTREACHED();
}
// LINT.ThenChange(//third_party/blink/public/mojom/ai/ai_manager.mojom:ModelAvailabilityCheckResult)
} // namespace blink