From 83f0483fe57409892c28954c89a87b594e83fbc7 Mon Sep 17 00:00:00 2001 From: Benjamin Congdon Date: Mon, 15 Dec 2025 06:59:50 -0800 Subject: [PATCH 1/3] Fix chat titles relying on Anthropic key --- src/core/chorus/api/MessageAPI.ts | 2 +- src/core/chorus/simpleLLM.ts | 120 ++++++++++++++++++++------ src/ui/components/ChatSuggestions.tsx | 1 - 3 files changed, 94 insertions(+), 29 deletions(-) diff --git a/src/core/chorus/api/MessageAPI.ts b/src/core/chorus/api/MessageAPI.ts index 50b380f2..d6528f8e 100644 --- a/src/core/chorus/api/MessageAPI.ts +++ b/src/core/chorus/api/MessageAPI.ts @@ -2918,6 +2918,7 @@ export function useGenerateChatTitle() { return { skipped: true }; } + // TODO: Consider moving this API call to the tauri backend. const fullResponse = await simpleLLM( `Based on this first message, write a 1-5 word title for the conversation. Try to put the most important words first. Format your response as YOUR TITLE HERE. If there's no information in the message, just return "Untitled Chat". @@ -2925,7 +2926,6 @@ If there's no information in the message, just return "Untitled Chat". ${userMessageText} `, { - model: "claude-3-5-sonnet-latest", maxTokens: 100, }, ); diff --git a/src/core/chorus/simpleLLM.ts b/src/core/chorus/simpleLLM.ts index c72961dc..0ad76e01 100644 --- a/src/core/chorus/simpleLLM.ts +++ b/src/core/chorus/simpleLLM.ts @@ -1,14 +1,25 @@ import Anthropic from "@anthropic-ai/sdk"; +import OpenAI from "openai"; import { SettingsManager } from "@core/utilities/Settings"; type SimpleLLMParams = { - model: string; + model?: SimpleLLMModel; maxTokens: number; }; +enum SimpleLLMModel { + CLAUDE_HAIKU_4_5 = "claude-haiku-4-5", +} + /** - * Makes a simple LLM call using the Anthropic SDK directly. - * Used for generating chat titles. + * Makes a simple LLM call. + * + * Prefers the Anthropic SDK when an Anthropic API key is configured. + * If no Anthropic key is available but an OpenRouter key is present, + * falls back to calling an Anthropic model via OpenRouter's + * OpenAI-compatible API. + * + * Used primarily for generating chat titles and suggestions. */ export async function simpleLLM( prompt: string, @@ -16,37 +27,92 @@ export async function simpleLLM( ): Promise { const settingsManager = SettingsManager.getInstance(); const settings = await settingsManager.get(); - const apiKey = settings.apiKeys?.anthropic; + const anthropicKey = settings.apiKeys?.anthropic; + const openrouterKey = settings.apiKeys?.openrouter; + + // Prefer direct Anthropic if available + if (anthropicKey) { + const client = new Anthropic({ + apiKey: anthropicKey, + dangerouslyAllowBrowser: true, + }); + + let anthropicModel: string; + switch (params.model ?? SimpleLLMModel.CLAUDE_HAIKU_4_5) { + case SimpleLLMModel.CLAUDE_HAIKU_4_5: + anthropicModel = "claude-haiku-4-5"; + break; + } - if (!apiKey) { - throw new Error("Please add your Anthropic API key in Settings."); + const stream = client.messages.stream({ + model: anthropicModel, + max_tokens: params.maxTokens, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + stream.on("text", (text: string) => { + fullResponse += text; + }); + + await stream.finalMessage(); + + return fullResponse; } - const client = new Anthropic({ - apiKey, - dangerouslyAllowBrowser: true, - }); - - const stream = client.messages.stream({ - model: params.model, - max_tokens: params.maxTokens, - messages: [ - { - role: "user", - content: prompt, + // Fallback: use OpenRouter via OpenAI-compatible API if available + if (openrouterKey) { + const client = new OpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey: openrouterKey, + defaultHeaders: { + "HTTP-Referer": "https://chorus.sh", + "X-Title": "Chorus", }, - ], - }); - - let fullResponse = ""; + dangerouslyAllowBrowser: true, + }); + + let openrouterModel: string; + switch (params.model ?? SimpleLLMModel.CLAUDE_HAIKU_4_5) { + case SimpleLLMModel.CLAUDE_HAIKU_4_5: + openrouterModel = "anthropic/claude-haiku-4.5"; + break; + } - stream.on("text", (text: string) => { - fullResponse += text; - }); + const stream = await client.chat.completions.create({ + model: openrouterModel, + max_tokens: params.maxTokens, + stream: true, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta?.content; + if (typeof delta === "string") { + fullResponse += delta; + } + } - await stream.finalMessage(); + return fullResponse; + } - return fullResponse; + // If neither key is available, surface a clear error + throw new Error( + "Please add an Anthropic or OpenRouter API key in Settings to generate chat titles.", + ); } /** diff --git a/src/ui/components/ChatSuggestions.tsx b/src/ui/components/ChatSuggestions.tsx index a9cdafd0..c85940a4 100644 --- a/src/ui/components/ChatSuggestions.tsx +++ b/src/ui/components/ChatSuggestions.tsx @@ -88,7 +88,6 @@ Generate exactly 2 suggestions as a JSON array of strings. Each suggestion shoul let response: string; try { response = await simpleLLM(prompt, { - model: "claude-3-5-sonnet-latest", maxTokens: 512, }); } catch (llmError) { From a00d41943cb6b676a45dc8386f4908524fba8da5 Mon Sep 17 00:00:00 2001 From: Benjamin Congdon Date: Mon, 15 Dec 2025 20:07:02 -0800 Subject: [PATCH 2/3] Refactor simpleLLM to use a unified provider system and remove deprecated code. Update API calls in MessageAPI and ProjectAPI to utilize the new completion mode for summarization. --- .../simple/ISimpleCompletionProvider.ts | 24 +++ .../SimpleCompletionProviderAnthropic.ts | 58 +++++ .../simple/SimpleCompletionProviderFactory.ts | 67 ++++++ .../simple/SimpleCompletionProviderGoogle.ts | 61 ++++++ .../simple/SimpleCompletionProviderOpenAI.ts | 60 ++++++ .../SimpleCompletionProviderOpenRouter.ts | 65 ++++++ src/core/chorus/api/MessageAPI.ts | 9 +- src/core/chorus/api/ProjectAPI.ts | 8 +- src/core/chorus/simpleLLM.ts | 200 ++---------------- 9 files changed, 358 insertions(+), 194 deletions(-) create mode 100644 src/core/chorus/ModelProviders/simple/ISimpleCompletionProvider.ts create mode 100644 src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts create mode 100644 src/core/chorus/ModelProviders/simple/SimpleCompletionProviderFactory.ts create mode 100644 src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts create mode 100644 src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts create mode 100644 src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts diff --git a/src/core/chorus/ModelProviders/simple/ISimpleCompletionProvider.ts b/src/core/chorus/ModelProviders/simple/ISimpleCompletionProvider.ts new file mode 100644 index 00000000..6786c97b --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/ISimpleCompletionProvider.ts @@ -0,0 +1,24 @@ +export enum SimpleCompletionMode { + TITLE_GENERATION = "title_generation", + SUMMARIZER = "summarizer", +} + +export type SimpleCompletionParams = { + model?: SimpleCompletionMode | string; + maxTokens: number; +}; + +/** + * Lightweight interface for simple LLM completions. + * Used for utility tasks like generating chat titles and suggestions. + * Intentionally separate from IProvider to avoid coupling to streaming/tools/attachments. + */ +export interface ISimpleCompletionProvider { + /** + * Performs a simple completion request. + * @param prompt The prompt to send to the model + * @param params Completion parameters including model and maxTokens + * @returns The full response text + */ + complete(prompt: string, params: SimpleCompletionParams): Promise; +} diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts new file mode 100644 index 00000000..1507fa32 --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts @@ -0,0 +1,58 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { + ISimpleCompletionProvider, + SimpleCompletionParams, + SimpleCompletionMode, +} from "./ISimpleCompletionProvider"; + +const DEFAULT_TITLE_MODEL = "claude-haiku-4-5"; +const DEFAULT_SUMMARIZER_MODEL = "claude-haiku-4-5"; + +export class SimpleCompletionProviderAnthropic + implements ISimpleCompletionProvider +{ + constructor(private apiKey: string) {} + + async complete( + prompt: string, + params: SimpleCompletionParams, + ): Promise { + const client = new Anthropic({ + apiKey: this.apiKey, + dangerouslyAllowBrowser: true, + }); + + const model = this.getModel(params.model); + + const stream = client.messages.stream({ + model, + max_tokens: params.maxTokens, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + stream.on("text", (text: string) => { + fullResponse += text; + }); + + await stream.finalMessage(); + + return fullResponse; + } + + private getModel(model: SimpleCompletionMode | string | undefined): string { + if (typeof model === "string") { + return model; + } + if (model === SimpleCompletionMode.SUMMARIZER) { + return DEFAULT_SUMMARIZER_MODEL; + } + return DEFAULT_TITLE_MODEL; + } +} diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderFactory.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderFactory.ts new file mode 100644 index 00000000..d81ffc47 --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderFactory.ts @@ -0,0 +1,67 @@ +import { ApiKeys } from "../../Models"; +import { canProceedWithProvider } from "@core/utilities/ProxyUtils"; +import { ISimpleCompletionProvider } from "./ISimpleCompletionProvider"; +import { SimpleCompletionProviderAnthropic } from "./SimpleCompletionProviderAnthropic"; +import { SimpleCompletionProviderOpenRouter } from "./SimpleCompletionProviderOpenRouter"; +import { SimpleCompletionProviderOpenAI } from "./SimpleCompletionProviderOpenAI"; +import { SimpleCompletionProviderGoogle } from "./SimpleCompletionProviderGoogle"; + +type ProviderConfig = { + name: string; + key: keyof ApiKeys; + create: (apiKey: string) => ISimpleCompletionProvider; +}; + +const PROVIDER_PRECEDENCE: ProviderConfig[] = [ + { + name: "anthropic", + key: "anthropic", + create: (key) => new SimpleCompletionProviderAnthropic(key), + }, + { + name: "openai", + key: "openai", + create: (key) => new SimpleCompletionProviderOpenAI(key), + }, + { + name: "google", + key: "google", + create: (key) => new SimpleCompletionProviderGoogle(key), + }, + { + name: "openrouter", + key: "openrouter", + create: (key) => new SimpleCompletionProviderOpenRouter(key), + }, +]; + +/** + * Factory function that selects and returns an appropriate simple completion provider + * based on available API keys. Follows explicit precedence order. + * + * @param apiKeys The API keys object from settings + * @returns An ISimpleCompletionProvider instance + * @throws Error if no suitable provider is configured + */ +export function getSimpleCompletionProvider( + apiKeys: ApiKeys, +): ISimpleCompletionProvider { + const reasons: string[] = []; + + for (const provider of PROVIDER_PRECEDENCE) { + const check = canProceedWithProvider(provider.name, apiKeys); + const apiKey = apiKeys[provider.key]; + + if (check.canProceed && apiKey) { + return provider.create(apiKey); + } + + if (!check.canProceed && check.reason) { + reasons.push(check.reason); + } + } + + throw new Error( + `Please add an Anthropic, OpenAI, Google, or OpenRouter API key in Settings to generate chat titles. ${reasons.join(" ")}`, + ); +} diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts new file mode 100644 index 00000000..34bf3240 --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts @@ -0,0 +1,61 @@ +import OpenAI from "openai"; +import { + ISimpleCompletionProvider, + SimpleCompletionParams, + SimpleCompletionMode, +} from "./ISimpleCompletionProvider"; + +const DEFAULT_TITLE_MODEL = "gemini-2.5-flash"; +const DEFAULT_SUMMARIZER_MODEL = "gemini-2.5-flash"; + +export class SimpleCompletionProviderGoogle + implements ISimpleCompletionProvider +{ + constructor(private apiKey: string) {} + + async complete( + prompt: string, + params: SimpleCompletionParams, + ): Promise { + const client = new OpenAI({ + baseURL: "https://generativelanguage.googleapis.com/v1beta/openai", + apiKey: this.apiKey, + dangerouslyAllowBrowser: true, + }); + + const model = this.getModel(params.model); + + const stream = await client.chat.completions.create({ + model, + max_tokens: params.maxTokens, + stream: true, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta?.content; + if (typeof delta === "string") { + fullResponse += delta; + } + } + + return fullResponse; + } + + private getModel(model: SimpleCompletionMode | string | undefined): string { + if (typeof model === "string") { + return model; + } + if (model === SimpleCompletionMode.SUMMARIZER) { + return DEFAULT_SUMMARIZER_MODEL; + } + return DEFAULT_TITLE_MODEL; + } +} diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts new file mode 100644 index 00000000..ff34fb1c --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts @@ -0,0 +1,60 @@ +import OpenAI from "openai"; +import { + ISimpleCompletionProvider, + SimpleCompletionParams, + SimpleCompletionMode, +} from "./ISimpleCompletionProvider"; + +const DEFAULT_TITLE_MODEL = "gpt-5-mini"; +const DEFAULT_SUMMARIZER_MODEL = "gpt-5-mini"; + +export class SimpleCompletionProviderOpenAI + implements ISimpleCompletionProvider +{ + constructor(private apiKey: string) {} + + async complete( + prompt: string, + params: SimpleCompletionParams, + ): Promise { + const client = new OpenAI({ + apiKey: this.apiKey, + dangerouslyAllowBrowser: true, + }); + + const model = this.getModel(params.model); + + const stream = await client.chat.completions.create({ + model, + max_tokens: params.maxTokens, + stream: true, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta?.content; + if (typeof delta === "string") { + fullResponse += delta; + } + } + + return fullResponse; + } + + private getModel(model: SimpleCompletionMode | string | undefined): string { + if (typeof model === "string") { + return model; + } + if (model === SimpleCompletionMode.SUMMARIZER) { + return DEFAULT_SUMMARIZER_MODEL; + } + return DEFAULT_TITLE_MODEL; + } +} diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts new file mode 100644 index 00000000..53e0798b --- /dev/null +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts @@ -0,0 +1,65 @@ +import OpenAI from "openai"; +import { + ISimpleCompletionProvider, + SimpleCompletionParams, + SimpleCompletionMode, +} from "./ISimpleCompletionProvider"; + +const DEFAULT_TITLE_MODEL = "anthropic/claude-haiku-4.5"; +const DEFAULT_SUMMARIZER_MODEL = "anthropic/claude-haiku-4.5"; + +export class SimpleCompletionProviderOpenRouter + implements ISimpleCompletionProvider +{ + constructor(private apiKey: string) {} + + async complete( + prompt: string, + params: SimpleCompletionParams, + ): Promise { + const client = new OpenAI({ + baseURL: "https://openrouter.ai/api/v1", + apiKey: this.apiKey, + defaultHeaders: { + "HTTP-Referer": "https://chorus.sh", + "X-Title": "Chorus", + }, + dangerouslyAllowBrowser: true, + }); + + const model = this.getModel(params.model); + + const stream = await client.chat.completions.create({ + model, + max_tokens: params.maxTokens, + stream: true, + messages: [ + { + role: "user", + content: prompt, + }, + ], + }); + + let fullResponse = ""; + + for await (const chunk of stream) { + const delta = chunk.choices?.[0]?.delta?.content; + if (typeof delta === "string") { + fullResponse += delta; + } + } + + return fullResponse; + } + + private getModel(model: SimpleCompletionMode | string | undefined): string { + if (typeof model === "string") { + return model; + } + if (model === SimpleCompletionMode.SUMMARIZER) { + return DEFAULT_SUMMARIZER_MODEL; + } + return DEFAULT_TITLE_MODEL; + } +} diff --git a/src/core/chorus/api/MessageAPI.ts b/src/core/chorus/api/MessageAPI.ts index d6528f8e..8e0505cb 100644 --- a/src/core/chorus/api/MessageAPI.ts +++ b/src/core/chorus/api/MessageAPI.ts @@ -21,7 +21,8 @@ import * as Models from "../Models"; import { UpdateQueue } from "../UpdateQueue"; import posthog from "posthog-js"; import { v4 as uuidv4 } from "uuid"; -import { simpleLLM, simpleSummarizeLLM } from "../simpleLLM"; +import { simpleLLM } from "../simpleLLM"; +import { SimpleCompletionMode } from "../ModelProviders/simple/ISimpleCompletionProvider"; import * as Prompts from "../prompts/prompts"; import { useNavigate } from "react-router-dom"; import { ToolsetsManager } from "../ToolsetsManager"; @@ -2164,9 +2165,8 @@ export function useSummarizeChat() { conversationText, ); - const summary = await simpleSummarizeLLM(prompt, { - // NOTE: If you change this model _provider_, you'll need to update the response handling in simpleSummarizeLLM.ts - model: "gemini-2.5-flash", + const summary = await simpleLLM(prompt, { + model: SimpleCompletionMode.SUMMARIZER, maxTokens: 8192, }); @@ -2918,7 +2918,6 @@ export function useGenerateChatTitle() { return { skipped: true }; } - // TODO: Consider moving this API call to the tauri backend. const fullResponse = await simpleLLM( `Based on this first message, write a 1-5 word title for the conversation. Try to put the most important words first. Format your response as YOUR TITLE HERE. If there's no information in the message, just return "Untitled Chat". diff --git a/src/core/chorus/api/ProjectAPI.ts b/src/core/chorus/api/ProjectAPI.ts index d6238b45..920b9a0e 100644 --- a/src/core/chorus/api/ProjectAPI.ts +++ b/src/core/chorus/api/ProjectAPI.ts @@ -6,7 +6,8 @@ import * as Prompts from "../prompts/prompts"; import { produce } from "immer"; import { useGetMessageSets } from "./MessageAPI"; import { llmConversation } from "../ChatState"; -import { simpleSummarizeLLM } from "../simpleLLM"; +import { simpleLLM } from "../simpleLLM"; +import { SimpleCompletionMode } from "../ModelProviders/simple/ISimpleCompletionProvider"; import _ from "lodash"; import { useNavigate } from "react-router-dom"; import { db } from "../DB"; @@ -401,11 +402,10 @@ function useRegenerateProjectContextSummary() { .map((m) => `${m.role}: ${m.content}`) .join("\n\n"); - const summary = await simpleSummarizeLLM( + const summary = await simpleLLM( Prompts.PROJECT_CONTEXT_SUMMARY_PROMPT(conversationText), { - // NOTE: If you change this model _provider_, you'll need to update the response handling in simpleSummarizeLLM.ts - model: "gemini-2.5-flash", + model: SimpleCompletionMode.SUMMARIZER, maxTokens: 8192, }, ); diff --git a/src/core/chorus/simpleLLM.ts b/src/core/chorus/simpleLLM.ts index 0ad76e01..c9591df4 100644 --- a/src/core/chorus/simpleLLM.ts +++ b/src/core/chorus/simpleLLM.ts @@ -1,198 +1,28 @@ -import Anthropic from "@anthropic-ai/sdk"; -import OpenAI from "openai"; import { SettingsManager } from "@core/utilities/Settings"; - -type SimpleLLMParams = { - model?: SimpleLLMModel; - maxTokens: number; -}; - -enum SimpleLLMModel { - CLAUDE_HAIKU_4_5 = "claude-haiku-4-5", -} +import { getSimpleCompletionProvider } from "./ModelProviders/simple/SimpleCompletionProviderFactory"; +import { + SimpleCompletionParams, + SimpleCompletionMode, +} from "./ModelProviders/simple/ISimpleCompletionProvider"; /** - * Makes a simple LLM call. - * - * Prefers the Anthropic SDK when an Anthropic API key is configured. - * If no Anthropic key is available but an OpenRouter key is present, - * falls back to calling an Anthropic model via OpenRouter's - * OpenAI-compatible API. - * + * Makes a simple LLM call using the first available provider. * Used primarily for generating chat titles and suggestions. */ export async function simpleLLM( prompt: string, - params: SimpleLLMParams, -): Promise { - const settingsManager = SettingsManager.getInstance(); - const settings = await settingsManager.get(); - const anthropicKey = settings.apiKeys?.anthropic; - const openrouterKey = settings.apiKeys?.openrouter; - - // Prefer direct Anthropic if available - if (anthropicKey) { - const client = new Anthropic({ - apiKey: anthropicKey, - dangerouslyAllowBrowser: true, - }); - - let anthropicModel: string; - switch (params.model ?? SimpleLLMModel.CLAUDE_HAIKU_4_5) { - case SimpleLLMModel.CLAUDE_HAIKU_4_5: - anthropicModel = "claude-haiku-4-5"; - break; - } - - const stream = client.messages.stream({ - model: anthropicModel, - max_tokens: params.maxTokens, - messages: [ - { - role: "user", - content: prompt, - }, - ], - }); - - let fullResponse = ""; - - stream.on("text", (text: string) => { - fullResponse += text; - }); - - await stream.finalMessage(); - - return fullResponse; - } - - // Fallback: use OpenRouter via OpenAI-compatible API if available - if (openrouterKey) { - const client = new OpenAI({ - baseURL: "https://openrouter.ai/api/v1", - apiKey: openrouterKey, - defaultHeaders: { - "HTTP-Referer": "https://chorus.sh", - "X-Title": "Chorus", - }, - dangerouslyAllowBrowser: true, - }); - - let openrouterModel: string; - switch (params.model ?? SimpleLLMModel.CLAUDE_HAIKU_4_5) { - case SimpleLLMModel.CLAUDE_HAIKU_4_5: - openrouterModel = "anthropic/claude-haiku-4.5"; - break; - } - - const stream = await client.chat.completions.create({ - model: openrouterModel, - max_tokens: params.maxTokens, - stream: true, - messages: [ - { - role: "user", - content: prompt, - }, - ], - }); - - let fullResponse = ""; - - for await (const chunk of stream) { - const delta = chunk.choices?.[0]?.delta?.content; - if (typeof delta === "string") { - fullResponse += delta; - } - } - - return fullResponse; - } - - // If neither key is available, surface a clear error - throw new Error( - "Please add an Anthropic or OpenRouter API key in Settings to generate chat titles.", - ); -} - -/** - * Makes a simple LLM call using Google's Gemini models via OpenAI-compatible API. - * Used for generating chat titles and summaries. - */ -export async function simpleSummarizeLLM( - prompt: string, - params: SimpleLLMParams, + params: SimpleCompletionParams, ): Promise { const settingsManager = SettingsManager.getInstance(); const settings = await settingsManager.get(); - const apiKey = settings.apiKeys?.google; - - if (!apiKey) { - throw new Error("Please add your Google AI API key in Settings."); - } - - // Use Google's OpenAI-compatible endpoint - const response = await fetch( - "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - model: params.model, - max_tokens: params.maxTokens, - stream: true, - messages: [ - { - role: "user", - content: prompt, - }, - ], - }), - }, - ); - - if (!response.ok) { - throw new Error(`Google API error: ${response.statusText}`); - } - - const reader = response.body?.getReader(); - if (!reader) throw new Error("No reader available"); - - const decoder = new TextDecoder(); - let fullResponse = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - const lines = chunk.split("\n"); - - for (const line of lines) { - if (line.startsWith("data: ")) { - try { - const dataStr = line.slice(6); - if (dataStr === "[DONE]") continue; - - const data = JSON.parse(dataStr) as { - choices?: Array<{ - delta?: { content?: string }; - }>; - }; + const apiKeys = settings.apiKeys || {}; - const content = data.choices?.[0]?.delta?.content; - if (content) { - fullResponse += content; - } - } catch (e) { - console.warn("Error parsing chunk:", e); - } - } - } - } + // Default to title generation mode if no model specified + const paramsWithMode: SimpleCompletionParams = { + ...params, + model: params.model ?? SimpleCompletionMode.TITLE_GENERATION, + }; - return fullResponse; + const provider = getSimpleCompletionProvider(apiKeys); + return provider.complete(prompt, paramsWithMode); } From 6ee2ab71f6207248c0ee26e0de48fde4cb48ac0d Mon Sep 17 00:00:00 2001 From: Benjamin Congdon Date: Fri, 19 Dec 2025 05:45:43 -0800 Subject: [PATCH 3/3] Address review comment --- .../simple/SimpleCompletionProviderAnthropic.ts | 9 ++++++--- .../simple/SimpleCompletionProviderGoogle.ts | 9 ++++++--- .../simple/SimpleCompletionProviderOpenAI.ts | 9 ++++++--- .../simple/SimpleCompletionProviderOpenRouter.ts | 9 ++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts index 1507fa32..6689691d 100644 --- a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderAnthropic.ts @@ -47,12 +47,15 @@ export class SimpleCompletionProviderAnthropic } private getModel(model: SimpleCompletionMode | string | undefined): string { - if (typeof model === "string") { - return model; - } if (model === SimpleCompletionMode.SUMMARIZER) { return DEFAULT_SUMMARIZER_MODEL; } + if (model === SimpleCompletionMode.TITLE_GENERATION) { + return DEFAULT_TITLE_MODEL; + } + if (typeof model === "string") { + return model; + } return DEFAULT_TITLE_MODEL; } } diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts index 34bf3240..2884c72d 100644 --- a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderGoogle.ts @@ -50,12 +50,15 @@ export class SimpleCompletionProviderGoogle } private getModel(model: SimpleCompletionMode | string | undefined): string { - if (typeof model === "string") { - return model; - } if (model === SimpleCompletionMode.SUMMARIZER) { return DEFAULT_SUMMARIZER_MODEL; } + if (model === SimpleCompletionMode.TITLE_GENERATION) { + return DEFAULT_TITLE_MODEL; + } + if (typeof model === "string") { + return model; + } return DEFAULT_TITLE_MODEL; } } diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts index ff34fb1c..71f68702 100644 --- a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenAI.ts @@ -49,12 +49,15 @@ export class SimpleCompletionProviderOpenAI } private getModel(model: SimpleCompletionMode | string | undefined): string { - if (typeof model === "string") { - return model; - } if (model === SimpleCompletionMode.SUMMARIZER) { return DEFAULT_SUMMARIZER_MODEL; } + if (model === SimpleCompletionMode.TITLE_GENERATION) { + return DEFAULT_TITLE_MODEL; + } + if (typeof model === "string") { + return model; + } return DEFAULT_TITLE_MODEL; } } diff --git a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts index 53e0798b..b599b549 100644 --- a/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts +++ b/src/core/chorus/ModelProviders/simple/SimpleCompletionProviderOpenRouter.ts @@ -54,12 +54,15 @@ export class SimpleCompletionProviderOpenRouter } private getModel(model: SimpleCompletionMode | string | undefined): string { - if (typeof model === "string") { - return model; - } if (model === SimpleCompletionMode.SUMMARIZER) { return DEFAULT_SUMMARIZER_MODEL; } + if (model === SimpleCompletionMode.TITLE_GENERATION) { + return DEFAULT_TITLE_MODEL; + } + if (typeof model === "string") { + return model; + } return DEFAULT_TITLE_MODEL; } }