Enable unsafe member access linting (#2474)

This commit is contained in:
Zero Liu 2026-05-15 18:26:40 -07:00 committed by GitHub
parent 3f12c14444
commit e03234137d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 118 additions and 141 deletions

View file

@ -103,10 +103,8 @@ export default [
// are candidates to enable in small follow-up PRs.
// --- Heavy: any-flow through Obsidian/LangChain APIs ---
// no-unsafe-member-access: enabled globally; tests and heavy source files
// are exempted via per-file overrides below (see "no-unsafe-member-access
// exemptions"). Remaining source files (≤5 violations each) were fixed in
// this PR.
// no-unsafe-member-access: enabled globally; tests are exempted via the
// test-file override below.
"@typescript-eslint/no-unsafe-assignment": "off", // enabled for tests below; follow-up PR for production
"@typescript-eslint/no-unsafe-call": "off", // 107 violations
@ -158,58 +156,6 @@ export default [
},
},
// no-unsafe-member-access exemptions: heavy source files that flow `any`
// through Obsidian / LangChain / Bedrock APIs. Counts are current as of the
// PR that enabled the rule; pick these off one at a time in follow-up PRs.
{
files: [
"src/LLMProviders/BedrockChatModel.ts", // 106
"src/LLMProviders/ChatOpenRouter.ts", // 28
"src/LLMProviders/CustomOpenAIEmbeddings.ts", // 16
"src/LLMProviders/brevilabsClient.ts", // 7
"src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts", // 13
"src/LLMProviders/chainRunner/BaseChainRunner.ts", // 7
"src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts", // 55
"src/LLMProviders/chainRunner/VaultQAChainRunner.ts", // 9
"src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts", // 8
"src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts", // 33
"src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts", // 17
"src/LLMProviders/chainRunner/utils/citationUtils.ts", // 11
"src/LLMProviders/chainRunner/utils/finishReasonDetector.ts", // 29
"src/LLMProviders/chainRunner/utils/modelAdapter.ts", // 9
"src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts", // 12
"src/LLMProviders/chainRunner/utils/searchResultUtils.ts", // 81
"src/LLMProviders/chainRunner/utils/toolExecution.ts", // 9
"src/LLMProviders/chatModelManager.ts", // 9
"src/LLMProviders/selfHostServices.ts", // 9
"src/commands/customCommandManager.ts", // 10
"src/commands/customCommandUtils.ts", // 10
"src/commands/index.ts", // 14
"src/components/chat-components/ChatControls.tsx", // 8
"src/components/chat-components/ChatInput.tsx", // 14
"src/components/modals/SourcesModal.tsx", // 33
"src/contextProcessor.ts", // 17
"src/core/ChatPersistenceManager.ts", // 10
"src/encryptionService.ts", // 6
"src/projects/projectUtils.ts", // 38
"src/search/chunkedStorage.ts", // 28
"src/search/dbOperations.ts", // 27
"src/search/hybridRetriever.ts", // 11
"src/search/indexOperations.ts", // 15
"src/search/v3/TieredLexicalRetriever.ts", // 9
"src/settings/providerModels.ts", // 20
"src/system-prompts/systemPromptUtils.ts", // 9
"src/tools/FileParserManager.ts", // 11
"src/tools/SearchTools.ts", // 11
"src/tools/ToolResultFormatter.ts", // 106
"src/utils.ts", // 49
"src/utils/rateLimitUtils.ts", // 10
],
rules: {
"@typescript-eslint/no-unsafe-member-access": "off",
},
},
// Tests have been cleaned of unsafe `any` assignments. Production code
// (~499 violations) is a follow-up; keep tests enforced.
{

View file

@ -448,13 +448,19 @@ export class ChatOpenRouter extends ChatOpenAI {
return undefined;
}
return toolCalls.map((call) => ({
name: call?.function?.name,
args: call?.function?.arguments,
id: call?.id,
index: call?.index,
type: "tool_call_chunk" as const,
}));
return toolCalls.map((rawCall) => {
const call = rawCall as
| { function?: { name?: string; arguments?: string }; id?: string; index?: number }
| null
| undefined;
return {
name: call?.function?.name,
args: call?.function?.arguments,
id: call?.id,
index: call?.index,
type: "tool_call_chunk" as const,
};
});
}
/**

View file

@ -55,13 +55,13 @@ export class CustomOpenAIEmbeddings extends OpenAIEmbeddings {
);
}
const responseData = await response.json();
const responseData = (await response.json()) as { data?: Array<{ embedding?: unknown }> };
if (!responseData.data || !Array.isArray(responseData.data)) {
throw new Error("Invalid API response format: missing or invalid data array");
}
return (responseData.data as Array<{ embedding?: unknown }>).map((item) => {
return responseData.data.map((item) => {
if (!item.embedding || !Array.isArray(item.embedding)) {
throw new Error("Invalid API response format: missing or invalid embedding array");
}

View file

@ -1030,9 +1030,15 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner {
})
);
for await (const chunk of stream) {
for await (const rawChunk of stream) {
if (abortController.signal.aborted) break;
const chunk = rawChunk as {
response_metadata?: { finish_reason?: string };
tool_call_chunks?: unknown;
content?: unknown;
};
// Check for MALFORMED_FUNCTION_CALL error - throw to trigger fallback
const finishReason = chunk.response_metadata?.finish_reason;
if (finishReason === "MALFORMED_FUNCTION_CALL") {

View file

@ -1224,7 +1224,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`;
}
try {
const parsed = JSON.parse(toolResult.result);
const parsed = JSON.parse(toolResult.result) as { type?: unknown; documents?: unknown };
const searchResults =
parsed &&
typeof parsed === "object" &&

View file

@ -334,7 +334,7 @@ export function normalizeCitations(content: string, map: Map<number, number>): s
});
// Handle multiple citations: [^n, ^m] -> [n, m]
result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList) => {
result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList: string) => {
// Split and process each number in the list
const processedNumbers = citationList
.split(",")

View file

@ -55,10 +55,10 @@ const GOOGLE_SAFETY_SETTINGS_BLOCK_NONE: SafetySetting[] = [
// vocabulary from tiktoken.pages.dev, which blocks all LLM calls when the CDN is
// unreachable. This char/4 estimation is the same fallback LangChain uses internally
// before tiktoken loads. Actual token usage comes from API response metadata.
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching a private prototype method requires any cast
(BaseLanguageModel.prototype as any).getNumTokens = async (
content: string | Array<{ type: string; text?: string }>
) => {
(
BaseLanguageModel.prototype as { getNumTokens: (...args: unknown[]) => Promise<number> }
).getNumTokens = async (content: string | Array<{ type: string; text?: string }>) => {
const text =
typeof content === "string"
? content

View file

@ -60,7 +60,9 @@ async function firecrawlSearch(query: string, apiKey: string): Promise<SelfHostW
throw new Error(`Firecrawl search failed (${response.status}): ${text}`);
}
const json = await response.json();
const json = (await response.json()) as {
data?: FirecrawlSearchResult[] | { web?: FirecrawlSearchResult[] };
};
// v2 returns { data: { web: [...] } }, older responses return { data: [...] }
const rawData = json?.data;
@ -113,9 +115,12 @@ async function perplexitySonarSearch(
throw new Error(`Perplexity Sonar search failed (${response.status}): ${text}`);
}
const json = await response.json();
const json = (await response.json()) as {
choices?: Array<{ message?: { content?: string } }>;
citations?: unknown;
};
const content = json?.choices?.[0]?.message?.content ?? "";
const citations: string[] = Array.isArray(json?.citations) ? json.citations : [];
const citations: string[] = Array.isArray(json?.citations) ? (json.citations as string[]) : [];
return { content, citations };
}
@ -154,7 +159,7 @@ export async function selfHostYoutube4llm(url: string): Promise<Youtube4llmRespo
});
if (response.status === 200) {
const json = await response.json();
const json = (await response.json()) as { content?: string };
const elapsed = Date.now() - startTime;
logInfo(`[selfHostYoutube4llm] transcript received in ${elapsed}ms`);
return {
@ -164,12 +169,12 @@ export async function selfHostYoutube4llm(url: string): Promise<Youtube4llmRespo
}
if (response.status === 201 || response.status === 202) {
const json = await response.json();
const json = (await response.json()) as { job_id?: string };
const jobId = json.job_id;
if (!jobId) {
throw new Error("Supadata returned async status but no job_id");
}
return await pollSupadataJob(jobId as string, apiKey, startTime);
return await pollSupadataJob(jobId, apiKey, startTime);
}
const text = await response.text();
@ -199,7 +204,7 @@ async function pollSupadataJob(
});
if (pollResponse.status === 200) {
const json = await pollResponse.json();
const json = (await pollResponse.json()) as { content?: string };
const elapsed = Date.now() - startTime;
logInfo(`[selfHostYoutube4llm] async transcript completed in ${elapsed}ms`);
return {

View file

@ -66,13 +66,16 @@ export class CustomCommandManager {
commandFile = await app.vault.create(filePath, command.content);
}
await app.fileManager.processFrontMatter(commandFile, (frontmatter) => {
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
});
await app.fileManager.processFrontMatter(
commandFile,
(frontmatter: Record<string, unknown>) => {
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
}
);
if (!mergedOptions.skipStoreUpdate) {
updateCachedCommand(command, command.title);
@ -126,13 +129,16 @@ export class CustomCommandManager {
if (commandFile instanceof TFile) {
await app.vault.modify(commandFile, command.content);
await app.fileManager.processFrontMatter(commandFile, (frontmatter) => {
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
});
await app.fileManager.processFrontMatter(
commandFile,
(frontmatter: Record<string, unknown>) => {
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu;
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order;
frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey;
frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs;
}
);
}
} finally {
removePendingFileWrite(filePath);

View file

@ -492,7 +492,7 @@ export function getNextCustomCommandOrder(): number {
export async function ensureCommandFrontmatter(file: TFile, command: CustomCommand) {
try {
addPendingFileWrite(file.path);
await app.fileManager.processFrontMatter(file, (frontmatter) => {
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
if (frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] == null) {
frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu;
}

View file

@ -41,7 +41,8 @@ function getSafeStorage(): SafeStorage | null {
if (safeStorageInternal) return safeStorageInternal;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
safeStorageInternal = require("electron")?.remote?.safeStorage as SafeStorage | null;
const electron = require("electron") as { remote?: { safeStorage?: SafeStorage } } | undefined;
safeStorageInternal = electron?.remote?.safeStorage ?? null;
return safeStorageInternal;
} catch {
return null;

View file

@ -62,7 +62,7 @@ export async function writeProjectFrontmatter(
const webUrls = splitUrlsStringToArray(project.contextSource?.webUrls || "");
const youtubeUrls = splitUrlsStringToArray(project.contextSource?.youtubeUrls || "");
await app.fileManager.processFrontMatter(file, (frontmatter) => {
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
// Reason: project.id is the stable logical identity, always set by createProject/migration.
// Do NOT fallback to folderName — with name-based folders, folderName is derived from
// project name, not id, so it cannot serve as an id substitute.
@ -423,7 +423,7 @@ export async function ensureProjectFrontmatter(
try {
if (!alreadyPending) addPendingFileWrite(file.path);
await app.fileManager.processFrontMatter(file, (frontmatter) => {
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
// Reason: do NOT fallback to record.folderName for id — with name-based folders,
// folderName is derived from project name, not id.
if (frontmatter[COPILOT_PROJECT_ID] == null && record.project.id) {

View file

@ -10,7 +10,6 @@ const LEGACY_INDEX_SUFFIX = ".json";
export interface ChunkMetadata {
numPartitions: number;
vectorLength: number;
schema: Record<string, string>;
lastModified: number;
documentPartitions: Record<string, number>;
@ -138,7 +137,6 @@ export class ChunkedStorage {
if (!rawDocs || rawDocs.length === 0) {
const metadata: ChunkMetadata = {
numPartitions,
vectorLength: db.schema.embedding.match(/\d+/)[0],
schema: db.schema,
lastModified: Date.now(),
documentPartitions: {},
@ -158,7 +156,6 @@ export class ChunkedStorage {
const metadata: ChunkMetadata = {
numPartitions,
vectorLength: db.schema.embedding.match(/\d+/)[0],
schema: db.schema,
lastModified: Date.now(),
documentPartitions: Object.fromEntries(
@ -242,7 +239,7 @@ export class ChunkedStorage {
}
} catch (error) {
console.error(`Error saving database:`, error);
throw new CustomError(`Failed to save database: ${error.message}`);
throw new CustomError(`Failed to save database: ${(error as Error).message}`);
}
}
@ -285,14 +282,19 @@ export class ChunkedStorage {
});
// Load and merge all partitions
let mergedData = null;
const allChunks = [];
type ChunkData = {
internalDocumentIDStore: { internalIdToId: string[] };
docs: { docs: Record<string, unknown>; count: number };
index: { vectorIndexes: { embedding: { vectors: Record<string, unknown> } } };
};
let mergedData: ChunkData | null = null;
const allChunks: ChunkData[] = [];
// First, load all chunks
for (let i = 0; i < metadata.numPartitions; i++) {
const chunkPath = this.getChunkPath(i);
if (await this.app.vault.adapter.exists(chunkPath)) {
const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath));
const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath)) as ChunkData;
allChunks.push(chunkData);
// First chunk contains global data
@ -313,7 +315,7 @@ export class ChunkedStorage {
for (const internalId of mergedData.internalDocumentIDStore.internalIdToId) {
// Find document in any chunk
const doc = allChunks
.flatMap((chunk) => Object.values(chunk.docs.docs as Record<string, unknown>))
.flatMap((chunk) => Object.values(chunk.docs.docs))
.find((doc) => (doc as Record<string, unknown>).id === internalId);
if (doc) {
@ -333,16 +335,13 @@ export class ChunkedStorage {
// upsert cycles. These "ghost" IDs cause position mismatches after load,
// where some user IDs point to wrong doc positions or undefined entries.
mergedData.internalDocumentIDStore.internalIdToId = Object.values(orderedDocs).map(
(doc: { id: string }): string => doc.id
(doc): string => (doc as { id: string }).id
);
// Merge vectors from all chunks
mergedData.index.vectorIndexes.embedding.vectors = Object.assign(
{},
...allChunks.map(
(chunk) =>
(chunk.index?.vectorIndexes?.embedding?.vectors as Record<string, unknown>) || {}
)
...allChunks.map((chunk) => chunk.index?.vectorIndexes?.embedding?.vectors || {})
);
// Load merged data into database
@ -350,7 +349,7 @@ export class ChunkedStorage {
return newDb;
} catch (error) {
console.error(`Error loading database:`, error);
throw new CustomError(`Failed to load database: ${error.message}`);
throw new CustomError(`Failed to load database: ${(error as Error).message}`);
}
}
@ -373,7 +372,7 @@ export class ChunkedStorage {
}
} catch (error) {
console.error(`Error clearing storage:`, error);
throw new CustomError(`Failed to clear storage: ${error.message}`);
throw new CustomError(`Failed to clear storage: ${(error as Error).message}`);
}
}

View file

@ -260,7 +260,7 @@ export class HybridRetriever extends BaseRetriever {
created_at: hit.document.created_at,
nchars: hit.document.nchars,
// Expose chunkId explicitly for cross-engine deduplication
chunkId: hit.document.metadata?.chunkId,
chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId,
},
})
);
@ -268,8 +268,8 @@ export class HybridRetriever extends BaseRetriever {
// Combine and deduplicate results
const combinedResults = [...dailyNoteResultsWithContext, ...timeIntervalDocuments];
const uniqueResults = Array.from(
new Set(combinedResults.map((doc): string => doc.metadata.id as string))
).map((id) => combinedResults.find((doc) => doc.metadata.id === id));
new Set(combinedResults.map((doc): string => (doc.metadata as { id: string }).id))
).map((id) => combinedResults.find((doc) => (doc.metadata as { id: string }).id === id));
return uniqueResults.filter((doc): doc is Document => doc !== undefined);
}
@ -316,7 +316,7 @@ export class HybridRetriever extends BaseRetriever {
created_at: hit.document.created_at,
nchars: hit.document.nchars,
// Expose chunkId explicitly for cross-engine deduplication
chunkId: hit.document.metadata?.chunkId,
chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId,
},
});
})

View file

@ -185,7 +185,7 @@ export async function ensurePromptFrontmatter(file: TFile, prompt: UserSystemPro
if (!alreadyPending) {
addPendingFileWrite(file.path);
}
await app.fileManager.processFrontMatter(file, (frontmatter) => {
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
if (frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] == null) {
frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] = createdMs;
}
@ -247,7 +247,7 @@ export async function updatePromptDefaultFlag(
if (!alreadyPending) {
addPendingFileWrite(file.path);
}
await app.fileManager.processFrontMatter(file, (frontmatter) => {
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
if (isDefault) {
frontmatter[COPILOT_SYSTEM_PROMPT_DEFAULT] = true;
} else {

View file

@ -371,13 +371,14 @@ export class Docs4LLMParser implements FileParser {
} else if (Array.isArray(docs4llmResponse.response)) {
// Handle array of documents from docs4llm
const markdownParts: string[] = [];
for (const doc of docs4llmResponse.response) {
if (doc.content) {
for (const rawDoc of docs4llmResponse.response) {
const doc = rawDoc as { content?: { md?: string; text?: string } } | null | undefined;
if (doc?.content) {
// Prioritize markdown content, then fallback to text content
if (doc.content.md) {
markdownParts.push(doc.content.md as string);
markdownParts.push(doc.content.md);
} else if (doc.content.text) {
markdownParts.push(doc.content.text as string);
markdownParts.push(doc.content.text);
}
}
}

View file

@ -411,7 +411,7 @@ export interface ChatHistoryEntry {
// TODO: Deprecated, use chatHistoryUtils.processRawChatHistory instead
export function extractChatHistory(memoryVariables: MemoryVariables): ChatHistoryEntry[] {
const chatHistory: ChatHistoryEntry[] = [];
const { history } = memoryVariables;
const history = memoryVariables.history as Array<{ content?: string }>;
for (let i = 0; i < history.length; i += 2) {
const userMessage = history[i]?.content || "";
@ -694,12 +694,21 @@ export async function safeFetch(
// Check if response is error status (only throw if throwOnHttpError is true)
if (throwOnHttpError && response.status >= 400) {
let errorJson;
type ErrorJson = {
detail?: { reason?: string; message?: string } | string;
reason?: string;
message?: string;
};
let errorJson: ErrorJson | null = null;
try {
errorJson = typeof response.json === "string" ? JSON.parse(response.json) : response.json;
errorJson = (
typeof response.json === "string" ? JSON.parse(response.json) : response.json
) as ErrorJson;
} catch {
try {
errorJson = typeof response.text === "string" ? JSON.parse(response.text) : response.text;
errorJson = (
typeof response.text === "string" ? JSON.parse(response.text) : response.text
) as ErrorJson;
} catch {
errorJson = null;
}
@ -710,15 +719,13 @@ export async function safeFetch(
error.json = errorJson;
// Handle nested error structure
if (
errorJson?.detail?.reason === "Invalid license key" ||
errorJson?.reason === "Invalid license key"
) {
const detail = errorJson && typeof errorJson.detail === "object" ? errorJson.detail : undefined;
if (detail?.reason === "Invalid license key" || errorJson?.reason === "Invalid license key") {
error.message = "Invalid license key";
} else if (errorJson?.detail?.message || errorJson?.message) {
const message = errorJson?.detail?.message || errorJson?.message;
const reason = errorJson?.detail?.reason || errorJson?.reason;
error.message = reason ? `${message}: ${reason}` : message;
} else if (detail?.message || errorJson?.message) {
const message = detail?.message || errorJson?.message;
const reason = detail?.reason || errorJson?.reason;
error.message = reason ? `${message}: ${reason}` : (message ?? "");
} else if (errorJson?.detail) {
error.message = JSON.stringify(errorJson.detail);
} else if (errorJson) {
@ -990,7 +997,7 @@ export async function checkLatestVersion(): Promise<{
url: "https://api.github.com/repos/logancyang/obsidian-copilot/releases/latest",
method: "GET",
});
const version = response.json.tag_name.replace("v", "");
const version = (response.json as { tag_name: string }).tag_name.replace("v", "");
return { version, error: null };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Failed to check for updates";
@ -1180,9 +1187,9 @@ export function extractTextFromChunk(content: unknown): string {
return content;
}
if (Array.isArray(content)) {
return content
return (content as Array<{ type?: string; text?: string }>)
.filter((item) => item.type === "text")
.map((item) => item.text as string)
.map((item) => item.text ?? "")
.join("");
}
// For any other type, return empty string
@ -1236,12 +1243,12 @@ export async function withSuppressedTokenWarnings<T>(fn: () => Promise<T>): Prom
try {
// Replace with filtered version
console.warn = function (...args) {
console.warn = function (...args: unknown[]) {
// Ignore token counting warnings
const first = args[0];
if (
args[0]?.includes &&
(args[0].includes("Failed to calculate number of tokens") ||
args[0].includes("Unknown model"))
typeof first === "string" &&
(first.includes("Failed to calculate number of tokens") || first.includes("Unknown model"))
) {
return;
}