Use Canonical Models to set context window sizes (#6723)
This commit is contained in:
+113
-102
@@ -741,6 +741,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/canonical-model-info": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_canonical_model_info",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ModelInfoQuery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Model information retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ModelInfoResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/check_provider": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -1095,36 +1125,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/pricing": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"super::routes::config_management"
|
||||
],
|
||||
"operationId": "get_pricing",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PricingQuery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Model pricing data retrieved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PricingResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/config/prompts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -5261,10 +5261,6 @@
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"fast_model": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
@@ -5334,6 +5330,89 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelInfoData": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider",
|
||||
"model",
|
||||
"context_limit",
|
||||
"currency"
|
||||
],
|
||||
"properties": {
|
||||
"cache_read_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"cache_write_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"context_limit": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"max_output_tokens": {
|
||||
"type": "integer",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double",
|
||||
"nullable": true
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelInfoQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ModelInfoResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"model_info": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ModelInfoData"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ParseRecipeRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -5397,74 +5476,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"PricingData": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider",
|
||||
"model",
|
||||
"input_token_cost",
|
||||
"output_token_cost",
|
||||
"currency"
|
||||
],
|
||||
"properties": {
|
||||
"context_length": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"currency": {
|
||||
"type": "string"
|
||||
},
|
||||
"input_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"output_token_cost": {
|
||||
"type": "number",
|
||||
"format": "double"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PricingQuery": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"provider",
|
||||
"model"
|
||||
],
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PricingResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pricing",
|
||||
"source"
|
||||
],
|
||||
"properties": {
|
||||
"pricing": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PricingData"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PrincipalType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -620,7 +620,6 @@ export type MessageMetadata = {
|
||||
|
||||
export type ModelConfig = {
|
||||
context_limit?: number | null;
|
||||
fast_model?: string | null;
|
||||
max_tokens?: number | null;
|
||||
model_name: string;
|
||||
/**
|
||||
@@ -664,6 +663,28 @@ export type ModelInfo = {
|
||||
supports_cache_control?: boolean | null;
|
||||
};
|
||||
|
||||
export type ModelInfoData = {
|
||||
cache_read_token_cost?: number | null;
|
||||
cache_write_token_cost?: number | null;
|
||||
context_limit: number;
|
||||
currency: string;
|
||||
input_token_cost?: number | null;
|
||||
max_output_tokens?: number | null;
|
||||
model: string;
|
||||
output_token_cost?: number | null;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type ModelInfoQuery = {
|
||||
model: string;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type ModelInfoResponse = {
|
||||
model_info?: ModelInfoData | null;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type ParseRecipeRequest = {
|
||||
content: string;
|
||||
};
|
||||
@@ -703,25 +724,6 @@ export type PermissionsMetadata = {
|
||||
microphone?: boolean;
|
||||
};
|
||||
|
||||
export type PricingData = {
|
||||
context_length?: number | null;
|
||||
currency: string;
|
||||
input_token_cost: number;
|
||||
model: string;
|
||||
output_token_cost: number;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type PricingQuery = {
|
||||
model: string;
|
||||
provider: string;
|
||||
};
|
||||
|
||||
export type PricingResponse = {
|
||||
pricing: Array<PricingData>;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type PrincipalType = 'Extension' | 'Tool';
|
||||
|
||||
export type PromptContentResponse = {
|
||||
@@ -1972,6 +1974,22 @@ export type BackupConfigResponses = {
|
||||
|
||||
export type BackupConfigResponse = BackupConfigResponses[keyof BackupConfigResponses];
|
||||
|
||||
export type GetCanonicalModelInfoData = {
|
||||
body: ModelInfoQuery;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/canonical-model-info';
|
||||
};
|
||||
|
||||
export type GetCanonicalModelInfoResponses = {
|
||||
/**
|
||||
* Model information retrieved successfully
|
||||
*/
|
||||
200: ModelInfoResponse;
|
||||
};
|
||||
|
||||
export type GetCanonicalModelInfoResponse = GetCanonicalModelInfoResponses[keyof GetCanonicalModelInfoResponses];
|
||||
|
||||
export type CheckProviderData = {
|
||||
body: CheckProviderRequest;
|
||||
path?: never;
|
||||
@@ -2245,22 +2263,6 @@ export type UpsertPermissionsResponses = {
|
||||
|
||||
export type UpsertPermissionsResponse = UpsertPermissionsResponses[keyof UpsertPermissionsResponses];
|
||||
|
||||
export type GetPricingData = {
|
||||
body: PricingQuery;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/config/pricing';
|
||||
};
|
||||
|
||||
export type GetPricingResponses = {
|
||||
/**
|
||||
* Model pricing data retrieved successfully
|
||||
*/
|
||||
200: PricingResponse;
|
||||
};
|
||||
|
||||
export type GetPricingResponse = GetPricingResponses[keyof GetPricingResponses];
|
||||
|
||||
export type GetPromptsData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { getNavigationShortcutText } from '../utils/keyboardShortcuts';
|
||||
import { UserInput, ImageData } from '../types/message';
|
||||
import { compressImageDataUrl } from '../utils/conversionUtils';
|
||||
import { fetchCanonicalModelInfo } from '../utils/canonical';
|
||||
|
||||
interface PastedImage {
|
||||
id: string;
|
||||
@@ -58,11 +59,6 @@ const TOOLS_MAX_SUGGESTED = 60; // max number of tools before we show a warning
|
||||
// Manual compact trigger message - must match backend constant
|
||||
const MANUAL_COMPACT_TRIGGER = '/compact';
|
||||
|
||||
interface ModelLimit {
|
||||
pattern: string;
|
||||
context_limit: number;
|
||||
}
|
||||
|
||||
interface ChatInputProps {
|
||||
sessionId: string | null;
|
||||
handleSubmit: (input: UserInput) => void;
|
||||
@@ -142,7 +138,7 @@ export default function ChatInput({
|
||||
const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
null
|
||||
) as React.RefObject<HTMLDivElement>;
|
||||
const { getProviders, read } = useConfig();
|
||||
const { getProviders } = useConfig();
|
||||
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
||||
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
|
||||
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
|
||||
@@ -367,28 +363,6 @@ export default function ChatInput({
|
||||
}
|
||||
}, [textAreaRef]);
|
||||
|
||||
// Load model limits from the API
|
||||
const getModelLimits = async () => {
|
||||
try {
|
||||
const response = await read('model-limits', false);
|
||||
if (response) {
|
||||
// The response is already parsed, no need for JSON.parse
|
||||
return response as ModelLimit[];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching model limits:', err);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const findModelLimit = (modelName: string, modelLimits: ModelLimit[]): number | null => {
|
||||
if (!modelName) return null;
|
||||
const matchingLimit = modelLimits.find((limit) =>
|
||||
modelName.toLowerCase().includes(limit.pattern.toLowerCase())
|
||||
);
|
||||
return matchingLimit ? matchingLimit.context_limit : null;
|
||||
};
|
||||
|
||||
// Load providers and get current model's token limit
|
||||
const loadProviderDetails = async () => {
|
||||
try {
|
||||
@@ -403,7 +377,7 @@ export default function ChatInput({
|
||||
return;
|
||||
}
|
||||
|
||||
// First, check predefined models from environment (highest priority)
|
||||
// Priority 1: Check predefined models from environment
|
||||
const predefinedModels = getPredefinedModelsFromEnv();
|
||||
const predefinedModel = predefinedModels.find((m) => m.name === model);
|
||||
if (predefinedModel?.context_limit) {
|
||||
@@ -412,12 +386,18 @@ export default function ChatInput({
|
||||
return;
|
||||
}
|
||||
|
||||
const providers = await getProviders(true);
|
||||
// Priority 2: Check canonical model info (source of truth)
|
||||
const canonicalInfo = await fetchCanonicalModelInfo(provider, model);
|
||||
if (canonicalInfo?.context_limit) {
|
||||
setTokenLimit(canonicalInfo.context_limit);
|
||||
setIsTokenLimitLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the provider details for the current provider
|
||||
// Priority 3: Fall back to provider metadata known_models (may be outdated)
|
||||
const providers = await getProviders(true);
|
||||
const currentProvider = providers.find((p) => p.name === provider);
|
||||
if (currentProvider?.metadata?.known_models) {
|
||||
// Find the model's token limit from the backend response
|
||||
const modelConfig = currentProvider.metadata.known_models.find((m) => m.name === model);
|
||||
if (modelConfig?.context_limit) {
|
||||
setTokenLimit(modelConfig.context_limit);
|
||||
@@ -426,16 +406,7 @@ export default function ChatInput({
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Use pattern matching logic if no exact model match was found
|
||||
const modelLimit = await getModelLimits();
|
||||
const fallbackLimit = findModelLimit(model as string, modelLimit);
|
||||
if (fallbackLimit !== null) {
|
||||
setTokenLimit(fallbackLimit);
|
||||
setIsTokenLimitLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no match found, use the default model limit
|
||||
// Priority 4: Use default if nothing else found
|
||||
setTokenLimit(TOKEN_LIMIT_DEFAULT);
|
||||
setIsTokenLimitLoaded(true);
|
||||
} catch (err) {
|
||||
@@ -1190,11 +1161,13 @@ export default function ChatInput({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col relative h-auto p-4 transition-colors ${disableAnimation ? '' : 'page-transition'
|
||||
} ${isFocused
|
||||
className={`flex flex-col relative h-auto p-4 transition-colors ${
|
||||
disableAnimation ? '' : 'page-transition'
|
||||
} ${
|
||||
isFocused
|
||||
? 'border-border-strong hover:border-border-strong'
|
||||
: 'border-border-default hover:border-border-default'
|
||||
} bg-background-default z-10 rounded-t-2xl`}
|
||||
} bg-background-default z-10 rounded-t-2xl`}
|
||||
data-drop-zone="true"
|
||||
onDrop={handleLocalDrop}
|
||||
onDragOver={handleLocalDragOver}
|
||||
@@ -1263,7 +1236,7 @@ export default function ChatInput({
|
||||
size="sm"
|
||||
shape="round"
|
||||
variant="outline"
|
||||
onClick={() => { }}
|
||||
onClick={() => {}}
|
||||
disabled={true}
|
||||
className="bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600 rounded-full px-6 py-2"
|
||||
>
|
||||
@@ -1310,12 +1283,13 @@ export default function ChatInput({
|
||||
}
|
||||
}}
|
||||
disabled={isTranscribing}
|
||||
className={`rounded-full px-6 py-2 ${isRecording
|
||||
? 'bg-red-500 text-white hover:bg-red-600 border-red-500'
|
||||
: isTranscribing
|
||||
? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600'
|
||||
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600'
|
||||
}`}
|
||||
className={`rounded-full px-6 py-2 ${
|
||||
isRecording
|
||||
? 'bg-red-500 text-white hover:bg-red-600 border-red-500'
|
||||
: isTranscribing
|
||||
? 'bg-slate-600 text-white cursor-not-allowed animate-pulse border-slate-600'
|
||||
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600'
|
||||
}`}
|
||||
>
|
||||
<Microphone />
|
||||
</Button>
|
||||
@@ -1353,10 +1327,11 @@ export default function ChatInput({
|
||||
shape="round"
|
||||
variant="outline"
|
||||
disabled={isSubmitButtonDisabled}
|
||||
className={`rounded-full px-10 py-2 flex items-center gap-2 ${isSubmitButtonDisabled
|
||||
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
|
||||
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
|
||||
}`}
|
||||
className={`rounded-full px-10 py-2 flex items-center gap-2 ${
|
||||
isSubmitButtonDisabled
|
||||
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
|
||||
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
<span className="text-sm">Send</span>
|
||||
|
||||
@@ -120,9 +120,7 @@ function McpAppWrapper({
|
||||
|
||||
const resultWithMeta = toolResponse?.toolResult as ToolResultWithMeta | undefined;
|
||||
const toolResult =
|
||||
resultWithMeta?.status === 'success' && resultWithMeta.value
|
||||
? resultWithMeta.value
|
||||
: undefined;
|
||||
resultWithMeta?.status === 'success' && resultWithMeta.value ? resultWithMeta.value : undefined;
|
||||
|
||||
if (!resourceUri) return null;
|
||||
if (requestWithMeta.toolCall.status !== 'success') return null;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { useModelAndProvider } from '../ModelAndProviderContext';
|
||||
import { CoinIcon } from '../icons';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
||||
import { fetchModelPricing } from '../../utils/pricing';
|
||||
import { PricingData } from '../../api';
|
||||
import { fetchCanonicalModelInfo } from '../../utils/canonical';
|
||||
import type { ModelInfoData } from '../../api';
|
||||
|
||||
interface CostTrackerProps {
|
||||
inputTokens?: number;
|
||||
@@ -19,7 +19,7 @@ interface CostTrackerProps {
|
||||
|
||||
export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }: CostTrackerProps) {
|
||||
const { currentModel, currentProvider } = useModelAndProvider();
|
||||
const [costInfo, setCostInfo] = useState<PricingData | null>(null);
|
||||
const [costInfo, setCostInfo] = useState<ModelInfoData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showPricing, setShowPricing] = useState(true);
|
||||
const [pricingFailed, setPricingFailed] = useState(false);
|
||||
@@ -45,7 +45,7 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const costData = await fetchModelPricing(currentProvider, currentModel);
|
||||
const costData = await fetchCanonicalModelInfo(currentProvider, currentModel);
|
||||
if (costData) {
|
||||
setCostInfo(costData);
|
||||
setPricingFailed(false);
|
||||
@@ -84,8 +84,8 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
costInfo &&
|
||||
(costInfo.input_token_cost !== undefined || costInfo.output_token_cost !== undefined)
|
||||
) {
|
||||
const currentInputCost = inputTokens * (costInfo.input_token_cost || 0);
|
||||
const currentOutputCost = outputTokens * (costInfo.output_token_cost || 0);
|
||||
const currentInputCost = (inputTokens * (costInfo.input_token_cost || 0)) / 1_000_000;
|
||||
const currentOutputCost = (outputTokens * (costInfo.output_token_cost || 0)) / 1_000_000;
|
||||
totalCost += currentInputCost + currentOutputCost;
|
||||
}
|
||||
|
||||
@@ -100,8 +100,8 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
return 0;
|
||||
}
|
||||
|
||||
const inputCost = inputTokens * (costInfo.input_token_cost || 0);
|
||||
const outputCost = outputTokens * (costInfo.output_token_cost || 0);
|
||||
const inputCost = (inputTokens * (costInfo.input_token_cost || 0)) / 1_000_000;
|
||||
const outputCost = (outputTokens * (costInfo.output_token_cost || 0)) / 1_000_000;
|
||||
const total = inputCost + outputCost;
|
||||
|
||||
return total;
|
||||
@@ -201,8 +201,9 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
// Add current model if it has costs
|
||||
if (costInfo && (inputTokens > 0 || outputTokens > 0)) {
|
||||
const currentCost =
|
||||
inputTokens * (costInfo.input_token_cost || 0) +
|
||||
outputTokens * (costInfo.output_token_cost || 0);
|
||||
(inputTokens * (costInfo.input_token_cost || 0) +
|
||||
outputTokens * (costInfo.output_token_cost || 0)) /
|
||||
1_000_000;
|
||||
if (currentCost > 0) {
|
||||
tooltip += `${currentProvider}/${currentModel} (current): ${costInfo.currency || '$'}${currentCost.toFixed(6)} (${inputTokens.toLocaleString()} in, ${outputTokens.toLocaleString()} out)\n`;
|
||||
}
|
||||
@@ -213,7 +214,7 @@ export function CostTracker({ inputTokens = 0, outputTokens = 0, sessionCosts }:
|
||||
}
|
||||
|
||||
// Default tooltip for single model
|
||||
return `Input: ${inputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${(inputTokens * (costInfo?.input_token_cost || 0)).toFixed(6)}) | Output: ${outputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${(outputTokens * (costInfo?.output_token_cost || 0)).toFixed(6)})`;
|
||||
return `Input: ${inputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((inputTokens * (costInfo?.input_token_cost || 0)) / 1_000_000).toFixed(6)}) | Output: ${outputTokens.toLocaleString()} tokens (${costInfo?.currency || '$'}${((outputTokens * (costInfo?.output_token_cost || 0)) / 1_000_000).toFixed(6)})`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -358,15 +358,17 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
const resp = await searchSessions({
|
||||
query: { query: debouncedSearchTerm },
|
||||
});
|
||||
|
||||
|
||||
if (resp.data) {
|
||||
// Response is Vec<Session> - sessions that match the search
|
||||
const matchedSessionIds = new Set(resp.data.map((s: { id: string }) => s.id));
|
||||
const filtered = sessions.filter((session) => matchedSessionIds.has(session.id));
|
||||
|
||||
|
||||
startTransition(() => {
|
||||
setFilteredSessions(filtered);
|
||||
setSearchResults(filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null);
|
||||
setSearchResults(
|
||||
filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function ExtensionModal({
|
||||
const trimmedNewKey = value.trim();
|
||||
const normalizedNewKey = trimmedNewKey.toLowerCase();
|
||||
const isDuplicate = formData.headers.some(
|
||||
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedNewKey,
|
||||
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedNewKey
|
||||
);
|
||||
if (isDuplicate && trimmedNewKey !== '') {
|
||||
return;
|
||||
|
||||
@@ -45,9 +45,7 @@ export default function HeadersSection({
|
||||
const valueEmpty = !newValue.trim();
|
||||
const keyHasSpaces = newKey.includes(' ');
|
||||
const normalizedNewKey = newKey.trim().toLowerCase();
|
||||
const isDuplicate = headers.some(
|
||||
h => h.key.trim().toLowerCase() === normalizedNewKey
|
||||
);
|
||||
const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedNewKey);
|
||||
|
||||
if (keyEmpty || valueEmpty) {
|
||||
setInvalidFields({
|
||||
|
||||
@@ -30,10 +30,7 @@ export default function ModelsBottomBar({
|
||||
setView,
|
||||
alerts,
|
||||
}: ModelsBottomBarProps) {
|
||||
const {
|
||||
currentModel,
|
||||
currentProvider,
|
||||
} = useModelAndProvider();
|
||||
const { currentModel, currentProvider } = useModelAndProvider();
|
||||
const currentModelInfo = useCurrentModelInfo();
|
||||
const { read, getProviders } = useConfig();
|
||||
const [displayProvider, setDisplayProvider] = useState<string | null>(null);
|
||||
|
||||
@@ -85,7 +85,9 @@ export const SwitchModelModal = ({
|
||||
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean };
|
||||
const [modelOptions, setModelOptions] = useState<{ options: ModelOption[] }[]>([]);
|
||||
const [provider, setProvider] = useState<string | null>(initialProvider || currentProvider || null);
|
||||
const [provider, setProvider] = useState<string | null>(
|
||||
initialProvider || currentProvider || null
|
||||
);
|
||||
const [model, setModel] = useState<string>(currentModel || '');
|
||||
const [isCustomModel, setIsCustomModel] = useState(false);
|
||||
const [validationErrors, setValidationErrors] = useState({
|
||||
|
||||
+5
-6
@@ -78,7 +78,7 @@ export default function CustomProviderForm({
|
||||
const valueEmpty = !newHeaderValue.trim();
|
||||
const keyHasSpaces = newHeaderKey.includes(' ');
|
||||
const normalizedNewKey = newHeaderKey.trim().toLowerCase();
|
||||
const isDuplicate = headers.some(h => h.key.trim().toLowerCase() === normalizedNewKey);
|
||||
const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedNewKey);
|
||||
|
||||
if (keyEmpty || valueEmpty) {
|
||||
setInvalidHeaderFields({
|
||||
@@ -125,7 +125,7 @@ export default function CustomProviderForm({
|
||||
}
|
||||
const normalizedValue = value.trim().toLowerCase();
|
||||
const isDuplicate = headers.some(
|
||||
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue,
|
||||
(h, i) => i !== index && h.key.trim().toLowerCase() === normalizedValue
|
||||
);
|
||||
if (isDuplicate && normalizedValue !== '') {
|
||||
return;
|
||||
@@ -177,9 +177,7 @@ export default function CustomProviderForm({
|
||||
if (newHeaderKey.trim() && newHeaderValue.trim()) {
|
||||
const keyHasSpaces = newHeaderKey.includes(' ');
|
||||
const normalizedPendingKey = newHeaderKey.trim().toLowerCase();
|
||||
const isDuplicate = headers.some(
|
||||
(h) => h.key.trim().toLowerCase() === normalizedPendingKey,
|
||||
);
|
||||
const isDuplicate = headers.some((h) => h.key.trim().toLowerCase() === normalizedPendingKey);
|
||||
|
||||
if (!keyHasSpaces && !isDuplicate) {
|
||||
allHeaders.push({ key: newHeaderKey, value: newHeaderValue });
|
||||
@@ -387,7 +385,8 @@ export default function CustomProviderForm({
|
||||
Custom Headers
|
||||
</label>
|
||||
<p className="text-xs text-textSubtle mb-4">
|
||||
Add custom HTTP headers to include in requests to the provider. Click the "+" button to add after filling both fields.
|
||||
Add custom HTTP headers to include in requests to the provider. Click the "+" button
|
||||
to add after filling both fields.
|
||||
</p>
|
||||
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
{headers.map((header, index) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useModelAndProvider } from '../components/ModelAndProviderContext';
|
||||
import { fetchModelPricing } from '../utils/pricing';
|
||||
import { fetchCanonicalModelInfo } from '../utils/canonical';
|
||||
import { Session } from '../api';
|
||||
|
||||
interface UseCostTrackingProps {
|
||||
@@ -42,13 +42,18 @@ export const useCostTracking = ({
|
||||
const prevKey = `${prevProviderRef.current}/${prevModelRef.current}`;
|
||||
|
||||
// Get pricing info for the previous model
|
||||
const prevCostInfo = await fetchModelPricing(prevProviderRef.current, prevModelRef.current);
|
||||
const prevCostInfo = await fetchCanonicalModelInfo(
|
||||
prevProviderRef.current,
|
||||
prevModelRef.current
|
||||
);
|
||||
|
||||
if (prevCostInfo) {
|
||||
const prevInputCost =
|
||||
(sessionInputTokens || localInputTokens) * (prevCostInfo.input_token_cost || 0);
|
||||
((sessionInputTokens || localInputTokens) * (prevCostInfo.input_token_cost || 0)) /
|
||||
1_000_000;
|
||||
const prevOutputCost =
|
||||
(sessionOutputTokens || localOutputTokens) * (prevCostInfo.output_token_cost || 0);
|
||||
((sessionOutputTokens || localOutputTokens) * (prevCostInfo.output_token_cost || 0)) /
|
||||
1_000_000;
|
||||
const prevTotalCost = prevInputCost + prevOutputCost;
|
||||
|
||||
// Save the accumulated costs for this model
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Utilities for fetching canonical model information from the backend
|
||||
*/
|
||||
|
||||
import { getCanonicalModelInfo, type ModelInfoData } from '../api';
|
||||
|
||||
/**
|
||||
* Fetch canonical model info (pricing + context limits) for a specific provider/model
|
||||
*/
|
||||
export async function fetchCanonicalModelInfo(
|
||||
provider: string,
|
||||
model: string
|
||||
): Promise<ModelInfoData | null> {
|
||||
try {
|
||||
const response = await getCanonicalModelInfo({
|
||||
body: { provider, model },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
return response.data.model_info ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { getPricing, PricingData } from '../api';
|
||||
|
||||
/**
|
||||
* Fetch pricing for a specific provider/model from the backend
|
||||
*/
|
||||
export async function fetchModelPricing(
|
||||
provider: string,
|
||||
model: string
|
||||
): Promise<PricingData | null> {
|
||||
try {
|
||||
const response = await getPricing({
|
||||
body: { provider, model },
|
||||
throwOnError: false,
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.data.pricing?.[0] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user