diff --git a/extensions/void/package-lock.json b/extensions/void/package-lock.json index 9aec96fa..7e012ead 100644 --- a/extensions/void/package-lock.json +++ b/extensions/void/package-lock.json @@ -23,7 +23,6 @@ "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/uuid": "^10.0.0", - "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vscode/test-cli": "^0.0.10", @@ -53,7 +52,7 @@ "uuid": "^10.0.0" }, "engines": { - "vscode": "^1.92.0" + "vscode": "*" } }, "node_modules/@alloc/quick-lru": { @@ -1257,13 +1256,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/vscode": { - "version": "1.92.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.92.0.tgz", - "integrity": "sha512-DcZoCj17RXlzB4XJ7IfKdPTcTGDLYvTOcTNkvtjXWF+K2TlKzHHkBEXNWQRpBIXixNEUgx39cQeTFunY0E2msw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -6024,6 +6016,14 @@ "node": ">=10" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/extensions/void/package.json b/extensions/void/package.json index 7322c277..5ef96e21 100644 --- a/extensions/void/package.json +++ b/extensions/void/package.json @@ -5,7 +5,7 @@ "description": "", "version": "0.0.1", "engines": { - "vscode": "^1.92.0" + "vscode": "*" }, "categories": [ "Other" @@ -127,7 +127,6 @@ "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@types/uuid": "^10.0.0", - "@types/vscode": "1.92.0", "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vscode/test-cli": "^0.0.10", diff --git a/extensions/void/src/common/sendLLMMessage.ts b/extensions/void/src/common/sendLLMMessage.ts index 8e4c59dd..71259613 100644 --- a/extensions/void/src/common/sendLLMMessage.ts +++ b/extensions/void/src/common/sendLLMMessage.ts @@ -1,6 +1,6 @@ import Anthropic from '@anthropic-ai/sdk'; import OpenAI from 'openai'; -import { Ollama } from 'ollama/browser' +import { Ollama } from 'ollama' import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { VoidConfig } from '../webviews/common/contextForConfig' @@ -11,36 +11,36 @@ export type OnText = (newText: string, fullText: string) => void export type OnFinalMessage = (input: string) => void export type LLMMessageAnthropic = { - role: 'user' | 'assistant', - content: string, + role: 'user' | 'assistant'; + content: string; } export type LLMMessage = { - role: 'system' | 'user' | 'assistant', - content: string, + role: 'system' | 'user' | 'assistant'; + content: string; } type SendLLMMessageFnTypeInternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: OnFinalMessage, - onError: (error: string) => void, - voidConfig: VoidConfig, - abortRef: AbortRef, + messages: LLMMessage[]; + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: (error: string) => void; + voidConfig: VoidConfig; + abortRef: AbortRef; }) => void type SendLLMMessageFnTypeExternal = (params: { - messages: LLMMessage[], - onText: OnText, - onFinalMessage: (fullText: string) => void, - onError: (error: string) => void, - voidConfig: VoidConfig | null, - abortRef: AbortRef, + messages: LLMMessage[]; + onText: OnText; + onFinalMessage: (fullText: string) => void; + onError: (error: string) => void; + voidConfig: VoidConfig | null; + abortRef: AbortRef; }) => void const parseMaxTokensStr = (maxTokensStr: string) => { // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN - let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) + const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) if (Number.isNaN(int)) return undefined return int @@ -79,7 +79,7 @@ const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFi stream.on('finalMessage', (claude_response) => { if (did_abort) return // stringify the response's content - let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n'); + const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); onFinalMessage(content) }) @@ -117,7 +117,7 @@ const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, o // remove system messages that get sent to Gemini // str of all system messages - let systemMessage = messages + const systemMessage = messages .filter(msg => msg.role === 'system') .map(msg => msg.content) .join('\n'); @@ -173,7 +173,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal let openai: OpenAI let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) + const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) if (voidConfig.default.whichApi === 'openAI') { openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); @@ -181,10 +181,10 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal } else if (voidConfig.default.whichApi === 'openRouter') { openai = new OpenAI({ - baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, + baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, defaultHeaders: { - "HTTP-Referer": 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. - "X-Title": 'Void Editor', // Optional. Shows in rankings on openrouter.ai. + 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. + 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, }); options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens } @@ -235,7 +235,7 @@ const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinal export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { let didAbort = false - let fullText = "" + let fullText = '' // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either abortRef.current = () => { @@ -289,9 +289,9 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin fetch('https://api.greptile.com/v2/query', { method: 'POST', headers: { - "Authorization": `Bearer ${voidConfig.greptile.apikey}`, - "X-Github-Token": `${voidConfig.greptile.githubPAT}`, - "Content-Type": `application/json`, + 'Authorization': `Bearer ${voidConfig.greptile.apikey}`, + 'X-Github-Token': `${voidConfig.greptile.githubPAT}`, + 'Content-Type': `application/json`, }, body: JSON.stringify({ messages, @@ -310,7 +310,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin if (didAbort) return - for (let response of responseArr) { + for (const response of responseArr) { const type: string = response['type'] const message = response['message'] @@ -321,7 +321,7 @@ const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFin onText(message, fullText) } else if (type === 'sources') { - const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null } + const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } fullText += filepath onText(filepath, fullText) } diff --git a/extensions/void/src/extension/applyDiffLazily.ts b/extensions/void/src/extension/applyDiffLazily.ts index 74bf2d99..50161bab 100644 --- a/extensions/void/src/extension/applyDiffLazily.ts +++ b/extensions/void/src/extension/applyDiffLazily.ts @@ -1,4 +1,5 @@ -import * as vscode from 'vscode'; +import type * as vscode from 'vscode'; + import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage'; import { DiffArea } from '../common/shared_types'; import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts'; diff --git a/extensions/void/src/extension/extension.ts b/extensions/void/src/extension/extension.ts index 8dbb1c86..6a29e5df 100644 --- a/extensions/void/src/extension/extension.ts +++ b/extensions/void/src/extension/extension.ts @@ -10,28 +10,6 @@ import { readFileContentOfUri } from './extensionLib/readFileContentOfUri'; import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider'; import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider'; -// this comes from vscode.proposed.editorInsets.d.ts -declare module 'vscode' { - export interface WebviewEditorInset { - readonly editor: vscode.TextEditor; - readonly line: number; - readonly height: number; - readonly webview: vscode.Webview; - readonly onDidDispose: Event; - dispose(): void; - } - export namespace window { - export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; - } -} - -// this comes from vscode.d.ts -declare module 'vscode' { - export namespace languages { - export function addInlineDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: Range): void; - } -} - const roundRangeToLines = (selection: vscode.Selection) => { let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1 return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER) diff --git a/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts b/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts index 8ef55198..a8e2c881 100644 --- a/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts +++ b/extensions/void/src/extension/providers/CtrlKWebviewProvider.ts @@ -3,22 +3,6 @@ import * as vscode from 'vscode'; import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML'; -// this comes from vscode.proposed.editorInsets.d.ts -declare module 'vscode' { - export interface WebviewEditorInset { - readonly editor: vscode.TextEditor; - readonly line: number; - readonly height: number; - readonly webview: vscode.Webview; - readonly onDidDispose: Event; - dispose(): void; - } - export namespace window { - export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset; - } -} - - export class CtrlKWebviewProvider { diff --git a/extensions/void/tsconfig.json b/extensions/void/tsconfig.json index 63e3afa9..0eac21ec 100644 --- a/extensions/void/tsconfig.json +++ b/extensions/void/tsconfig.json @@ -1,6 +1,7 @@ { "include": [ - "src/**/*" + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", ], "exclude": [ "node_modules" @@ -27,4 +28,4 @@ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ } -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts b/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts index e69de29b..3bf99b11 100644 --- a/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts +++ b/src/vs/editor/browser/services/inlineDiffService/sendLLMMessage.ts @@ -0,0 +1,365 @@ +// import Anthropic from '@anthropic-ai/sdk'; +// import OpenAI from 'openai'; +// import { Ollama } from 'ollama' +// import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai'; +// import { VoidConfig } from '../webviews/common/contextForConfig' + +// export type AbortRef = { current: (() => void) | null } + +// export type OnText = (newText: string, fullText: string) => void + +// export type OnFinalMessage = (input: string) => void + +// export type LLMMessageAnthropic = { +// role: 'user' | 'assistant'; +// content: string; +// } + +// export type LLMMessage = { +// role: 'system' | 'user' | 'assistant'; +// content: string; +// } + +// type SendLLMMessageFnTypeInternal = (params: { +// messages: LLMMessage[]; +// onText: OnText; +// onFinalMessage: OnFinalMessage; +// onError: (error: string) => void; +// voidConfig: VoidConfig; +// abortRef: AbortRef; +// }) => void + +// type SendLLMMessageFnTypeExternal = (params: { +// messages: LLMMessage[]; +// onText: OnText; +// onFinalMessage: (fullText: string) => void; +// onError: (error: string) => void; +// voidConfig: VoidConfig | null; +// abortRef: AbortRef; +// }) => void + +// const parseMaxTokensStr = (maxTokensStr: string) => { +// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN +// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) +// if (Number.isNaN(int)) +// return undefined +// return int +// } + +// // Anthropic +// const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => { + +// const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"] + +// // find system messages and concatenate them +// const systemMessage = messages +// .filter(msg => msg.role === 'system') +// .map(msg => msg.content) +// .join('\n'); + +// // remove system messages for Anthropic +// const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[] + +// const stream = anthropic.messages.stream({ +// system: systemMessage, +// messages: anthropicMessages, +// model: voidConfig.anthropic.model, +// max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user +// }); + +// let did_abort = false + +// // when receive text +// stream.on('text', (newText, fullText) => { +// if (did_abort) return +// onText(newText, fullText) +// }) + +// // when we get the final message on this stream (or when error/fail) +// stream.on('finalMessage', (claude_response) => { +// if (did_abort) return +// // stringify the response's content +// const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n'); +// onFinalMessage(content) +// }) + +// stream.on('error', (error) => { +// // the most common error will be invalid API key (401), so we handle this with a nice message +// if (error instanceof Anthropic.APIError && error.status === 401) { +// onError('Invalid API key.') +// } +// else { +// onError(error.message) +// } +// }) + +// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either +// const abort = () => { +// did_abort = true +// stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error +// } + +// return { abort } +// }; + +// // Gemini +// const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { + +// let didAbort = false +// let fullText = '' + +// abortRef.current = () => { +// didAbort = true +// } + +// const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey); +// const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model }); + +// // remove system messages that get sent to Gemini +// // str of all system messages +// const systemMessage = messages +// .filter(msg => msg.role === 'system') +// .map(msg => msg.content) +// .join('\n'); + +// // Convert messages to Gemini format +// const geminiMessages: Content[] = messages +// .filter(msg => msg.role !== 'system') +// .map((msg, i) => ({ +// parts: [{ text: msg.content }], +// role: msg.role === 'assistant' ? 'model' : 'user' +// })) + +// model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, }) +// .then(async response => { +// abortRef.current = () => { +// // response.stream.return(fullText) +// didAbort = true; +// } +// for await (const chunk of response.stream) { +// if (didAbort) return; +// const newText = chunk.text(); +// fullText += newText; +// onText(newText, fullText); +// } +// onFinalMessage(fullText); +// }) +// .catch((error) => { +// if (error instanceof GoogleGenerativeAIFetchError) { +// if (error.status === 400) { +// onError('Invalid API key.'); +// } +// else { +// onError(`${error.name}:\n${error.message}`); +// } +// } +// else { +// onError(error); +// } +// }) +// } + +// // OpenAI, OpenRouter, OpenAICompatible +// const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { + +// let didAbort = false +// let fullText = '' + +// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either +// abortRef.current = () => { +// didAbort = true; +// }; + +// let openai: OpenAI +// let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming + +// const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens) + +// if (voidConfig.default.whichApi === 'openAI') { +// openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true }); +// options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens } +// } +// else if (voidConfig.default.whichApi === 'openRouter') { +// openai = new OpenAI({ +// baseURL: 'https://openrouter.ai/api/v1', apiKey: voidConfig.openRouter.apikey, dangerouslyAllowBrowser: true, +// defaultHeaders: { +// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. +// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. +// }, +// }); +// options = { model: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens } +// } +// else if (voidConfig.default.whichApi === 'openAICompatible') { +// openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true }) +// options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens } +// } +// else { +// console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`) +// throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`) +// } + +// openai.chat.completions +// .create(options) +// .then(async response => { +// abortRef.current = () => { +// // response.controller.abort() +// didAbort = true; +// } +// // when receive text +// for await (const chunk of response) { +// if (didAbort) return; +// const newText = chunk.choices[0]?.delta?.content || ''; +// fullText += newText; +// onText(newText, fullText); +// } +// onFinalMessage(fullText); +// }) +// // when error/fail - this catches errors of both .create() and .then(for await) +// .catch(error => { +// if (error instanceof OpenAI.APIError) { +// if (error.status === 401) { +// onError('Invalid API key.'); +// } +// else { +// onError(`${error.name}:\n${error.message}`); +// } +// } +// else { +// onError(error); +// } +// }) + +// }; + +// // Ollama +// export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { + +// let didAbort = false +// let fullText = '' + +// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either +// abortRef.current = () => { +// didAbort = true; +// }; + +// const ollama = new Ollama({ host: voidConfig.ollama.endpoint }) + +// ollama.chat({ +// model: voidConfig.ollama.model, +// messages: messages, +// stream: true, +// options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens +// }) +// .then(async stream => { +// abortRef.current = () => { +// // stream.abort() +// didAbort = true +// } +// // iterate through the stream +// for await (const chunk of stream) { +// if (didAbort) return; +// const newText = chunk.message.content; +// fullText += newText; +// onText(newText, fullText); +// } +// onFinalMessage(fullText); + +// }) +// // when error/fail +// .catch(error => { +// onError(error) +// }) + +// }; + +// // Greptile +// // https://docs.greptile.com/api-reference/query +// // https://docs.greptile.com/quickstart#sample-response-streamed + +// const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { + +// let didAbort = false +// let fullText = '' + +// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either +// abortRef.current = () => { +// didAbort = true +// } + +// fetch('https://api.greptile.com/v2/query', { +// method: 'POST', +// headers: { +// 'Authorization': `Bearer ${voidConfig.greptile.apikey}`, +// 'X-Github-Token': `${voidConfig.greptile.githubPAT}`, +// 'Content-Type': `application/json`, +// }, +// body: JSON.stringify({ +// messages, +// stream: true, +// repositories: [voidConfig.greptile.repoinfo], +// }), +// }) +// // this is {message}\n{message}\n{message}...\n +// .then(async response => { +// const text = await response.text() +// console.log('got greptile', text) +// return JSON.parse(`[${text.trim().split('\n').join(',')}]`) +// }) +// // TODO make this actually stream, right now it just sends one message at the end +// .then(async responseArr => { +// if (didAbort) +// return + +// for (const response of responseArr) { + +// const type: string = response['type'] +// const message = response['message'] + +// // when receive text +// if (type === 'message') { +// fullText += message +// onText(message, fullText) +// } +// else if (type === 'sources') { +// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null } +// fullText += filepath +// onText(filepath, fullText) +// } +// // type: 'status' with an empty 'message' means last message +// else if (type === 'status') { +// if (!message) { +// onFinalMessage(fullText) +// } +// } +// } + +// }) +// .catch(e => { +// onError(e) +// }); + +// } + +// export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => { +// if (!voidConfig) return; + +// // trim message content (Anthropic and other providers give an error if there is trailing whitespace) +// messages = messages.map(m => ({ ...m, content: m.content.trim() })) + +// switch (voidConfig.default.whichApi) { +// case 'anthropic': +// return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); +// case 'openAI': +// case 'openRouter': +// case 'openAICompatible': +// return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); +// case 'gemini': +// return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); +// case 'ollama': +// return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); +// case 'greptile': +// return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }); +// default: +// onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`) +// } +// }