mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
* fix: defense-in-depth overrides to prevent tiktoken CDN timeout in Plus mode (#2282) Plus mode's planToolCalls uses invoke() on a streaming model, which causes LangChain's _generate() to call _getEstimatedTokenCountFromPrompt after streaming. This triggers getNumTokens -> encodingForModel -> CDN fetch with 6 retries, hanging for minutes when tiktoken.pages.dev is unreachable. The existing getNumTokens instance override (PR #2160) is bypassed in some production bundle configurations. Add overrides at two additional levels: getNumTokensFromMessages and _getEstimatedTokenCountFromPrompt. The latter returns 0 since actual token usage comes from API response metadata. Agent mode is unaffected because it uses stream() which bypasses _generate() entirely. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use stream() instead of invoke() in planToolCalls to avoid tiktoken CDN fetch Instance property overrides on getNumTokens are bypassed in esbuild production bundles (prototype methods called directly). Instead of trying to override methods, eliminate the problematic code path: replace invoke() with stream() + chunk collection in planToolCalls. invoke() goes through _generate() which, with streaming: true, calls _getEstimatedTokenCountFromPrompt -> getNumTokensFromMessages -> getNumTokens -> encodingForModel -> tiktoken CDN fetch (6 retries). stream() goes through _streamResponseChunks() directly, bypassing _generate() and the entire token estimation path. This is the same approach Agent mode uses, which is why it was never affected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: patch getNumTokens on prototype instead of instance for esbuild compat Instance property overrides are bypassed in esbuild production bundles because minified code resolves this.method() calls directly to the prototype. Patch the prototype with a guard flag to ensure the tiktoken CDN fetch is prevented in all code paths including production builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: patch tiktoken on BaseLanguageModel prototype and convert invoke to stream Two fixes for the tiktoken CDN timeout that blocks web search: 1. Walk up the prototype chain to patch getNumTokens on BaseLanguageModel.prototype (where it's defined) instead of the direct prototype. One patch now covers ALL model classes (ChatOpenAI, ChatOpenRouter, etc.) instead of only the first class instantiated. 2. Convert getStandaloneQuestion() from invoke() to stream() to avoid _generate() -> _getEstimatedTokenCountFromPrompt() -> getNumTokens() -> tiktoken CDN fetch path entirely. This is the invoke() call triggered during @websearch tool execution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: simplify tiktoken patch to one-time module-level override Replace the per-instance prototype walking with a single module-level patch on BaseLanguageModel.prototype.getNumTokens. Runs at import time before any model is created, covering all model classes and all code paths (createModelInstance, ping, etc.) with zero per-call overhead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: handle array content in getStandaloneQuestion stream loop Use extractTextFromChunk() instead of a bare string check so that providers sending content-part arrays (e.g. Anthropic) are handled correctly during streaming. Previously array content was silently dropped, which could produce an empty condensed question. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
55 lines
2 KiB
TypeScript
55 lines
2 KiB
TypeScript
import ProjectManager from "@/LLMProviders/projectManager";
|
|
import {
|
|
ChatHistoryEntry,
|
|
extractTextFromChunk,
|
|
removeErrorTags,
|
|
removeThinkTags,
|
|
withSuppressedTokenWarnings,
|
|
} from "@/utils";
|
|
|
|
export async function getStandaloneQuestion(
|
|
question: string,
|
|
chatHistory: ChatHistoryEntry[]
|
|
): Promise<string> {
|
|
const condenseQuestionTemplate = `Given the following conversation and a follow up question,
|
|
summarize the conversation as context and keep the follow up question unchanged, in its original language.
|
|
If the follow up question is unrelated to its preceding messages, return this follow up question directly.
|
|
If it is related, then combine the summary and the follow up question to construct a standalone question.
|
|
Make sure to keep any [[]] wrapped note titles in the question unchanged.
|
|
If there's nothing in the chat history, just return the follow up question.
|
|
|
|
Chat History:
|
|
{chat_history}
|
|
Follow Up Input: {question}
|
|
Standalone question:`;
|
|
|
|
const formattedChatHistory = chatHistory
|
|
.map(({ role, content }) => `${role}: ${content}`)
|
|
.join("\n");
|
|
|
|
// Wrap the model call with token warning suppression
|
|
return await withSuppressedTokenWarnings(async () => {
|
|
// Use temperature=0 for deterministic question condensation
|
|
const chatModel = await ProjectManager.instance
|
|
.getCurrentChainManager()
|
|
.chatModelManager.getChatModelWithTemperature(0);
|
|
|
|
// Use stream() instead of invoke() to avoid LangChain's _generate() path,
|
|
// which triggers tiktoken CDN fetch via _getEstimatedTokenCountFromPrompt.
|
|
let text = "";
|
|
const stream = await chatModel.stream([
|
|
{
|
|
role: "user",
|
|
content: condenseQuestionTemplate
|
|
.replace("{chat_history}", formattedChatHistory)
|
|
.replace("{question}", question),
|
|
},
|
|
]);
|
|
for await (const chunk of stream) {
|
|
text += extractTextFromChunk(chunk.content);
|
|
}
|
|
|
|
const cleanedResponse = removeThinkTags(text);
|
|
return removeErrorTags(cleanedResponse);
|
|
});
|
|
}
|