feat(llm): basic cost multiplier

This commit is contained in:
Elian Doran
2026-03-29 14:44:40 +03:00
parent d8c00ed6c0
commit 5fcea86b94
4 changed files with 37 additions and 3 deletions

View File

@@ -41,6 +41,11 @@ interface LlmChatContent {
enableExtendedThinking?: boolean;
}
/** Extended model info with cost description for dropdown display */
interface ModelOption extends LlmModelInfo {
costDescription?: string;
}
export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
const [messages, setMessages] = useState<StoredMessage[]>([]);
const [input, setInput] = useState("");
@@ -49,7 +54,7 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
const [streamingThinking, setStreamingThinking] = useState("");
const [toolActivity, setToolActivity] = useState<string | null>(null);
const [pendingCitations, setPendingCitations] = useState<LlmCitation[]>([]);
const [availableModels, setAvailableModels] = useState<LlmModelInfo[]>([]);
const [availableModels, setAvailableModels] = useState<ModelOption[]>([]);
const [selectedModel, setSelectedModel] = useState<string>("");
const [enableWebSearch, setEnableWebSearch] = useState(true);
const [enableNoteTools, setEnableNoteTools] = useState(false);
@@ -62,7 +67,14 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
// Fetch available models on mount
useEffect(() => {
getAvailableModels().then(models => {
setAvailableModels(models);
// Add cost description for display
const modelsWithDescription = models.map(m => ({
...m,
costDescription: m.costMultiplier
? `${m.costMultiplier}x cost`
: undefined
}));
setAvailableModels(modelsWithDescription);
// Set default model if not already selected
if (!selectedModel) {
const defaultModel = models.find(m => m.isDefault) || models[0];
@@ -384,6 +396,7 @@ export default function LlmChat({ note, ntxId, noteContext }: TypeWidgetProps) {
values={availableModels}
keyProperty="id"
titleProperty="name"
descriptionProperty="costDescription"
currentValue={selectedModel}
onChange={handleModelChange}
disabled={isStreaming}

View File

@@ -8,10 +8,18 @@ import type { LlmProvider, LlmProviderConfig, ModelInfo, ModelPricing, StreamRes
const DEFAULT_MODEL = "claude-sonnet-4-20250514";
const DEFAULT_MAX_TOKENS = 8096;
/**
* Calculate effective cost for comparison (weighted average: 1 input + 3 output).
* Output is weighted more heavily as it's typically the dominant cost factor.
*/
function effectiveCost(pricing: ModelPricing): number {
return (pricing.input + 3 * pricing.output) / 4;
}
/**
* Available Anthropic models with pricing (USD per million tokens).
*/
const AVAILABLE_MODELS: ModelInfo[] = [
const BASE_MODELS: Omit<ModelInfo, "costMultiplier">[] = [
// Claude 4 family
{
id: "claude-sonnet-4-20250514",
@@ -37,6 +45,15 @@ const AVAILABLE_MODELS: ModelInfo[] = [
}
];
// Find cheapest model as baseline for cost multiplier
const baselineCost = Math.min(...BASE_MODELS.map(m => effectiveCost(m.pricing)));
// Build models with cost multipliers
const AVAILABLE_MODELS: ModelInfo[] = BASE_MODELS.map(m => ({
...m,
costMultiplier: Math.round((effectiveCost(m.pricing) / baselineCost) * 10) / 10
}));
// Build pricing lookup from available models
const MODEL_PRICING: Record<string, ModelPricing> = Object.fromEntries(
AVAILABLE_MODELS.map(m => [m.id, m.pricing])

View File

@@ -42,6 +42,8 @@ export interface ModelInfo {
pricing: ModelPricing;
/** Whether this is the default model */
isDefault?: boolean;
/** Cost multiplier relative to the cheapest model (1x = cheapest) */
costMultiplier?: number;
}
export interface LlmProvider {

View File

@@ -63,6 +63,8 @@ export interface LlmModelInfo {
pricing: LlmModelPricing;
/** Whether this is the default model */
isDefault?: boolean;
/** Cost multiplier relative to the cheapest model (1x = cheapest) */
costMultiplier?: number;
}
/**