diff --git a/extension/llm/apple/ExecuTorchLLM/Exported/ExecuTorchLLMTextRunner.mm b/extension/llm/apple/ExecuTorchLLM/Exported/ExecuTorchLLMTextRunner.mm index ea858f1df9e..9cb5f5d5f27 100644 --- a/extension/llm/apple/ExecuTorchLLM/Exported/ExecuTorchLLMTextRunner.mm +++ b/extension/llm/apple/ExecuTorchLLM/Exported/ExecuTorchLLMTextRunner.mm @@ -56,9 +56,31 @@ - (BOOL)isLoaded { - (BOOL)loadWithError:(NSError**)error { if (![self isLoaded]) { + // The loader pins the begin of sequence index at 0 and the end of sequence + // index at 1, so one entry cannot name both. Caught here because the loader + // reports it as a tokenizer that would not load, which hides the cause. + if (_specialTokens->size() == 1) { + if (error) { + *error = [NSError errorWithDomain:ExecuTorchLLMErrorDomain + code:-1 + userInfo:@{NSLocalizedDescriptionKey: @"Special tokens must name a begin and an end of sequence token, or be empty"}]; + } + return NO; + } + // An empty list means the caller had none to give, not that the tokenizer + // should have none. Copy rather than move so a retry after a failed load + // still has them. + std::unique_ptr> specialTokens; + if (!_specialTokens->empty()) { + specialTokens = + std::make_unique>(*_specialTokens); + } _runner = llm::create_text_llm_runner( _modelPath.UTF8String ?: "", - llm::load_tokenizer(_tokenizerPath.UTF8String ?: "", std::move(_specialTokens)) + llm::load_tokenizer( + _tokenizerPath.UTF8String ?: "", + std::move(specialTokens) + ) ); if (!_runner) { if (error) {