orchid-ai 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli/components/ChatPanel.d.ts +2 -1
- package/dist/cli/components/Conversation.d.ts +2 -1
- package/dist/cli/components/ModelSwitcher.d.ts +2 -1
- package/dist/cli/hooks/useModelSwitcher.d.ts +7 -4
- package/dist/cli/hooks/useResolvedDefaultModel.d.ts +21 -0
- package/dist/cli/hooks/useStreamingAI.d.ts +3 -2
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/server/contextual-service.d.ts +49 -1
- package/dist/cli/server/intent-detection.d.ts +2 -0
- package/dist/cli/server/utils.d.ts +0 -12
- package/dist/cli/types/types.d.ts +2 -1
- package/dist/components/ChatPanel.d.ts +2 -1
- package/dist/components/Conversation.d.ts +2 -1
- package/dist/components/ModelSwitcher.d.ts +2 -1
- package/dist/hooks/useModelSwitcher.d.ts +7 -4
- package/dist/hooks/useResolvedDefaultModel.d.ts +21 -0
- package/dist/hooks/useStreamingAI.d.ts +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +425 -39
- package/dist/index.js +425 -38
- package/dist/server/components/ChatPanel.d.ts +2 -1
- package/dist/server/components/Conversation.d.ts +2 -1
- package/dist/server/components/ModelSwitcher.d.ts +2 -1
- package/dist/server/contextual-service.d.ts +49 -1
- package/dist/server/hooks/useModelSwitcher.d.ts +7 -4
- package/dist/server/hooks/useResolvedDefaultModel.d.ts +21 -0
- package/dist/server/hooks/useStreamingAI.d.ts +3 -2
- package/dist/server/index.esm.js +507 -151
- package/dist/server/index.js +473 -99
- package/dist/server/intent-detection.d.ts +2 -0
- package/dist/server/server/contextual-service.d.ts +49 -1
- package/dist/server/server/intent-detection.d.ts +2 -0
- package/dist/server/server/utils.d.ts +0 -12
- package/dist/server/types/types.d.ts +2 -1
- package/dist/server/utils.d.ts +0 -12
- package/dist/types/types.d.ts +2 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createPortal } from 'react-dom';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CONFIG = {
|
|
6
6
|
service: 'claude',
|
|
7
|
-
model: 'claude-sonnet-4-
|
|
7
|
+
model: 'claude-sonnet-4-5-20250929', // Updated to use newer 4.5 model
|
|
8
8
|
temperature: 0.7,
|
|
9
9
|
maxTokens: 4096,
|
|
10
10
|
chatLevel: 'none',
|
|
@@ -40,28 +40,28 @@ const DEFAULT_MODELS = {
|
|
|
40
40
|
],
|
|
41
41
|
claude: [
|
|
42
42
|
{
|
|
43
|
-
id: 'claude-
|
|
44
|
-
name: 'Claude
|
|
43
|
+
id: 'claude-haiku-4-5-20251001',
|
|
44
|
+
name: 'Claude Haiku 4.5',
|
|
45
45
|
provider: 'claude',
|
|
46
46
|
available: true,
|
|
47
47
|
supportsImages: true,
|
|
48
|
-
computeWeight:
|
|
48
|
+
computeWeight: 0.2,
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
-
id: 'claude-sonnet-4-
|
|
52
|
-
name: 'Claude Sonnet 4',
|
|
51
|
+
id: 'claude-sonnet-4-5-20250929',
|
|
52
|
+
name: 'Claude Sonnet 4.5',
|
|
53
53
|
provider: 'claude',
|
|
54
54
|
available: true,
|
|
55
55
|
supportsImages: true,
|
|
56
56
|
computeWeight: 0.6,
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
|
-
id: 'claude-
|
|
60
|
-
name: 'Claude
|
|
59
|
+
id: 'claude-opus-4-20250514',
|
|
60
|
+
name: 'Claude Opus 4',
|
|
61
61
|
provider: 'claude',
|
|
62
62
|
available: true,
|
|
63
63
|
supportsImages: true,
|
|
64
|
-
computeWeight:
|
|
64
|
+
computeWeight: 1.2,
|
|
65
65
|
},
|
|
66
66
|
],
|
|
67
67
|
gemini: [
|
|
@@ -402,6 +402,45 @@ function useStreamingAI({ userId, serverConfig, formData, chats: externalChats,
|
|
|
402
402
|
additionalContext ? (typeof additionalContext === 'string' ? additionalContext : JSON.stringify(additionalContext)) : '',
|
|
403
403
|
context || ''
|
|
404
404
|
].filter(Boolean).join('\n\n');
|
|
405
|
+
// Prepare model selection for request
|
|
406
|
+
// Safety: ensure model matches provider; if mismatched, drop/replace model to avoid provider/model conflicts (e.g., Gemini with Claude model ID)
|
|
407
|
+
const normalizeModelSelection = (sel) => {
|
|
408
|
+
const provider = (sel.provider || '').toLowerCase();
|
|
409
|
+
const model = sel.model || '';
|
|
410
|
+
// Heuristic: if model prefix clearly belongs to another provider, treat as mismatch
|
|
411
|
+
const belongsToProvider = (prov, m) => {
|
|
412
|
+
if (!m)
|
|
413
|
+
return true;
|
|
414
|
+
const id = m.toLowerCase();
|
|
415
|
+
if (prov === 'gemini' || prov === 'google')
|
|
416
|
+
return id.startsWith('gemini');
|
|
417
|
+
if (prov === 'claude' || prov === 'anthropic')
|
|
418
|
+
return id.startsWith('claude');
|
|
419
|
+
if (prov === 'openai')
|
|
420
|
+
return id.startsWith('gpt');
|
|
421
|
+
return true;
|
|
422
|
+
};
|
|
423
|
+
if (!belongsToProvider(provider, model)) {
|
|
424
|
+
// Mismatch: prefer tier if provided, otherwise drop model so server can choose; as last resort, use DEFAULT_CONFIG when same provider
|
|
425
|
+
const normalized = { provider };
|
|
426
|
+
if (sel.tier)
|
|
427
|
+
normalized.tier = sel.tier;
|
|
428
|
+
// Only keep model if it matches provider
|
|
429
|
+
return normalized;
|
|
430
|
+
}
|
|
431
|
+
return sel;
|
|
432
|
+
};
|
|
433
|
+
const requestModelSelection = modelSelection
|
|
434
|
+
? normalizeModelSelection({
|
|
435
|
+
provider: modelSelection.provider,
|
|
436
|
+
...(modelSelection.model ? { model: modelSelection.model } : {}),
|
|
437
|
+
...(modelSelection.tier ? { tier: modelSelection.tier } : {}),
|
|
438
|
+
})
|
|
439
|
+
: {
|
|
440
|
+
provider: DEFAULT_CONFIG.service,
|
|
441
|
+
model: DEFAULT_CONFIG.model,
|
|
442
|
+
};
|
|
443
|
+
// Request log removed; rely on verbose-controlled logger if needed
|
|
405
444
|
const requestBody = {
|
|
406
445
|
query,
|
|
407
446
|
userId,
|
|
@@ -410,14 +449,7 @@ function useStreamingAI({ userId, serverConfig, formData, chats: externalChats,
|
|
|
410
449
|
formData: formData || {},
|
|
411
450
|
files: processedFiles,
|
|
412
451
|
// New: Include model selection in request
|
|
413
|
-
modelSelection:
|
|
414
|
-
provider: DEFAULT_CONFIG.service,
|
|
415
|
-
model: DEFAULT_CONFIG.model,
|
|
416
|
-
capabilities: {
|
|
417
|
-
supportsImages: DEFAULT_CONFIG.supportsImages,
|
|
418
|
-
computeWeight: 1.0,
|
|
419
|
-
},
|
|
420
|
-
},
|
|
452
|
+
modelSelection: requestModelSelection,
|
|
421
453
|
...(schema ? { schema } : {}), // <-- NEW: Include schema from serverConfig!
|
|
422
454
|
...(mergedContext ? { additionalContext: mergedContext } : {}), // <-- Include merged context
|
|
423
455
|
};
|
|
@@ -1068,6 +1100,65 @@ function useDebouncedSuggestions(query, options) {
|
|
|
1068
1100
|
};
|
|
1069
1101
|
}
|
|
1070
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Hook to resolve tier-based default model config to actual model
|
|
1105
|
+
*
|
|
1106
|
+
* This hook takes a default model configuration that may include a tier
|
|
1107
|
+
* (like 'fast', 'balanced', 'powerful') and resolves it to an actual model
|
|
1108
|
+
* by fetching the available models from the server and selecting the best
|
|
1109
|
+
* match for the provider and tier combination.
|
|
1110
|
+
*
|
|
1111
|
+
* @param defaultModel - The model configuration to resolve
|
|
1112
|
+
* @returns The resolved model with provider, model name, and capabilities
|
|
1113
|
+
*/
|
|
1114
|
+
function useResolvedDefaultModel(defaultModel) {
|
|
1115
|
+
const [resolvedModel, setResolvedModel] = useState();
|
|
1116
|
+
useEffect(() => {
|
|
1117
|
+
if (!defaultModel || defaultModel.model) {
|
|
1118
|
+
// If no config or explicit model provided, set resolved model directly
|
|
1119
|
+
if (defaultModel) {
|
|
1120
|
+
setResolvedModel({
|
|
1121
|
+
provider: defaultModel.provider,
|
|
1122
|
+
model: defaultModel.model,
|
|
1123
|
+
capabilities: { supportsImages: false, computeWeight: 1.0 }, // Default capabilities
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
// Need to resolve tier to actual model
|
|
1129
|
+
const resolveModel = async () => {
|
|
1130
|
+
try {
|
|
1131
|
+
const response = await fetch('/command/models');
|
|
1132
|
+
if (!response.ok)
|
|
1133
|
+
throw new Error('Failed to fetch models');
|
|
1134
|
+
const data = await response.json();
|
|
1135
|
+
const modelsByTier = data.modelsByTier || {};
|
|
1136
|
+
// Get the model for the requested provider and tier
|
|
1137
|
+
const providerModels = modelsByTier[defaultModel.provider];
|
|
1138
|
+
if (providerModels && providerModels[defaultModel.tier]) {
|
|
1139
|
+
const modelName = providerModels[defaultModel.tier];
|
|
1140
|
+
setResolvedModel({
|
|
1141
|
+
provider: defaultModel.provider,
|
|
1142
|
+
model: modelName,
|
|
1143
|
+
capabilities: { supportsImages: false, computeWeight: 1.0 }, // Will be updated by server
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
catch (error) {
|
|
1148
|
+
console.error('Failed to resolve default model:', error);
|
|
1149
|
+
// Fallback to a reasonable default
|
|
1150
|
+
setResolvedModel({
|
|
1151
|
+
provider: defaultModel.provider,
|
|
1152
|
+
model: `${defaultModel.provider}-default`,
|
|
1153
|
+
capabilities: { supportsImages: false, computeWeight: 1.0 },
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
resolveModel();
|
|
1158
|
+
}, [defaultModel]);
|
|
1159
|
+
return resolvedModel;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1071
1162
|
// Size mapping
|
|
1072
1163
|
const sizeMap = {
|
|
1073
1164
|
xs: 12,
|
|
@@ -1927,14 +2018,68 @@ const FileHandler = {
|
|
|
1927
2018
|
},
|
|
1928
2019
|
};
|
|
1929
2020
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2021
|
+
// Helper function to build URL from config (reused from useStreamingAI)
|
|
2022
|
+
function buildUrlFromConfig(config) {
|
|
2023
|
+
if (typeof window === 'undefined')
|
|
2024
|
+
return config.suffix || '/api';
|
|
2025
|
+
const protocol = config.secure !== undefined
|
|
2026
|
+
? config.secure
|
|
2027
|
+
? 'https:'
|
|
2028
|
+
: 'http:'
|
|
2029
|
+
: window.location.protocol;
|
|
2030
|
+
const hostname = config.domain || window.location.hostname;
|
|
2031
|
+
const port = config.port
|
|
2032
|
+
? `:${config.port}`
|
|
2033
|
+
: window.location.port
|
|
2034
|
+
? `:${window.location.port}`
|
|
2035
|
+
: '';
|
|
2036
|
+
const suffix = config.suffix || '';
|
|
2037
|
+
// If no domain specified, use relative URL (common with webpack proxy)
|
|
2038
|
+
if (!config.domain && !config.port && !config.secure) {
|
|
2039
|
+
return suffix;
|
|
2040
|
+
}
|
|
2041
|
+
return `${protocol}//${hostname}${port}${suffix}`;
|
|
2042
|
+
}
|
|
2043
|
+
function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialModel, initialTier, // Can be undefined - don't default here, let Conversation handle it
|
|
2044
|
+
customModels, serverConfig, } = {}) {
|
|
2045
|
+
// Use provided initialModel if available
|
|
2046
|
+
// If tier is specified without model, use empty string (server will handle tier-based selection)
|
|
2047
|
+
// DO NOT use DEFAULT_CONFIG.model here - it will interfere with tier-based selection
|
|
2048
|
+
// We'll only auto-select a model later if no tier is specified AND models are available
|
|
2049
|
+
const effectiveInitialModel = initialModel || '';
|
|
2050
|
+
// State for auto-fetched models
|
|
2051
|
+
const [fetchedModels, setFetchedModels] = useState(null);
|
|
2052
|
+
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
|
2053
|
+
const [modelsError, setModelsError] = useState(null);
|
|
2054
|
+
// Track if user explicitly switched models; prevents defaultModel overrides after first user action
|
|
2055
|
+
const [hasUserSwitched, setHasUserSwitched] = useState(false);
|
|
2056
|
+
// Determine which models to use
|
|
2057
|
+
// Priority: customModels > fetchedModels > DEFAULT_MODELS
|
|
2058
|
+
const availableModels = customModels || fetchedModels || DEFAULT_MODELS;
|
|
1933
2059
|
// Simple state management - no API calls
|
|
2060
|
+
// If tier is specified without explicit model, use empty string to signal tier-based selection
|
|
2061
|
+
// If no tier and no model, use effectiveInitialModel (which may be DEFAULT_CONFIG.model as last resort)
|
|
1934
2062
|
const [currentModel, setCurrentModel] = useState({
|
|
1935
2063
|
provider: initialProvider,
|
|
1936
|
-
model:
|
|
2064
|
+
model: effectiveInitialModel,
|
|
1937
2065
|
});
|
|
2066
|
+
// Update model when initialTier/initialModel change (e.g., when defaultModel prop becomes available)
|
|
2067
|
+
useEffect(() => {
|
|
2068
|
+
if (hasUserSwitched) {
|
|
2069
|
+
// Respect user choice; do not override with defaults after first manual switch
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (initialTier && !initialModel) {
|
|
2073
|
+
// Tier-based selection: clear model so server handles it
|
|
2074
|
+
if (currentModel.model !== '') {
|
|
2075
|
+
setCurrentModel(prev => ({ ...prev, model: '' }));
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
else if (initialModel && currentModel.model !== initialModel) {
|
|
2079
|
+
// Explicit model specified: use it
|
|
2080
|
+
setCurrentModel(prev => ({ ...prev, model: initialModel }));
|
|
2081
|
+
}
|
|
2082
|
+
}, [initialTier, initialModel, currentModel.model, hasUserSwitched]);
|
|
1938
2083
|
const [usageStats, setUsageStats] = useState({
|
|
1939
2084
|
requests: 0,
|
|
1940
2085
|
totalTokens: 0,
|
|
@@ -1943,10 +2088,126 @@ function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialMod
|
|
|
1943
2088
|
uptime: 0,
|
|
1944
2089
|
providers: [],
|
|
1945
2090
|
currentProvider: initialProvider,
|
|
1946
|
-
currentModel:
|
|
2091
|
+
currentModel: effectiveInitialModel,
|
|
1947
2092
|
});
|
|
1948
2093
|
// Track session start time for uptime calculation
|
|
1949
2094
|
const [sessionStartTime] = useState(Date.now());
|
|
2095
|
+
// Auto-fetch models from server if customModels not provided and serverConfig is available
|
|
2096
|
+
useEffect(() => {
|
|
2097
|
+
// Only fetch if:
|
|
2098
|
+
// 1. No custom models provided
|
|
2099
|
+
// 2. Server config is available
|
|
2100
|
+
// 3. Haven't fetched yet
|
|
2101
|
+
if (customModels || !serverConfig || fetchedModels !== null || isLoadingModels) {
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const fetchModels = async () => {
|
|
2105
|
+
setIsLoadingModels(true);
|
|
2106
|
+
setModelsError(null);
|
|
2107
|
+
try {
|
|
2108
|
+
const apiUrl = buildUrlFromConfig(serverConfig);
|
|
2109
|
+
const modelsEndpoint = `${apiUrl}/models`;
|
|
2110
|
+
const response = await fetch(modelsEndpoint);
|
|
2111
|
+
if (!response.ok) {
|
|
2112
|
+
throw new Error(`Failed to fetch models: ${response.statusText}`);
|
|
2113
|
+
}
|
|
2114
|
+
const data = await response.json();
|
|
2115
|
+
if (data.status === 'ok' && data.models) {
|
|
2116
|
+
// Transform server response into the format expected by the hook
|
|
2117
|
+
const modelsByProvider = {};
|
|
2118
|
+
// Group models by provider
|
|
2119
|
+
data.models.forEach((model) => {
|
|
2120
|
+
const provider = model.provider.toLowerCase();
|
|
2121
|
+
if (!modelsByProvider[provider]) {
|
|
2122
|
+
modelsByProvider[provider] = [];
|
|
2123
|
+
}
|
|
2124
|
+
modelsByProvider[provider].push({
|
|
2125
|
+
id: model.id,
|
|
2126
|
+
name: model.name || model.id,
|
|
2127
|
+
provider: provider,
|
|
2128
|
+
available: model.available !== false,
|
|
2129
|
+
supportsImages: model.supportsImages || false,
|
|
2130
|
+
computeWeight: model.computeWeight || 0.5,
|
|
2131
|
+
description: model.description || '',
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
setFetchedModels(modelsByProvider);
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
throw new Error('Invalid response format from models endpoint');
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
catch (error) {
|
|
2141
|
+
console.warn('⚠️ Could not auto-fetch models from server, using defaults:', error);
|
|
2142
|
+
setModelsError(error instanceof Error ? error.message : 'Unknown error');
|
|
2143
|
+
// Continue with DEFAULT_MODELS as fallback
|
|
2144
|
+
setFetchedModels({}); // Set to empty object to prevent retry
|
|
2145
|
+
}
|
|
2146
|
+
finally {
|
|
2147
|
+
setIsLoadingModels(false);
|
|
2148
|
+
}
|
|
2149
|
+
};
|
|
2150
|
+
fetchModels();
|
|
2151
|
+
}, [customModels, serverConfig, fetchedModels, isLoadingModels]);
|
|
2152
|
+
// Update initial model if we fetched models and need to auto-select from tier
|
|
2153
|
+
// BUT: If initialTier is specified, we should NOT auto-select here - let server handle tier selection
|
|
2154
|
+
useEffect(() => {
|
|
2155
|
+
// Only auto-select if:
|
|
2156
|
+
// 1. We have fetched models
|
|
2157
|
+
// 2. We're using the default initial model (or no model specified)
|
|
2158
|
+
// 3. NO tier is specified (tier-based selection should happen server-side)
|
|
2159
|
+
// 4. Current model still has the initial model
|
|
2160
|
+
if (fetchedModels && Object.keys(fetchedModels).length > 0) {
|
|
2161
|
+
// If tier is specified, don't auto-select - server will handle tier selection
|
|
2162
|
+
// This prevents the hook from selecting a model when tier-based selection is desired
|
|
2163
|
+
if (initialTier) {
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
// Auto-select only if:
|
|
2167
|
+
// 1. No tier specified (tier-based selection should be server-side)
|
|
2168
|
+
// 2. No explicit model provided
|
|
2169
|
+
// 3. Current model is empty (needs selection)
|
|
2170
|
+
// This ensures we pick the cheapest model when no preference is given
|
|
2171
|
+
const shouldAutoSelect = (!initialTier) && // Don't auto-select if tier is specified
|
|
2172
|
+
(!initialModel) && // Don't auto-select if explicit model provided
|
|
2173
|
+
currentModel.model === ''; // Only if empty (need to select something)
|
|
2174
|
+
if (shouldAutoSelect) {
|
|
2175
|
+
const providerModels = fetchedModels[initialProvider];
|
|
2176
|
+
if (providerModels && providerModels.length > 0) {
|
|
2177
|
+
// Sort models by computeWeight
|
|
2178
|
+
const sorted = [...providerModels].sort((a, b) => {
|
|
2179
|
+
const weightA = a.computeWeight || 0.5;
|
|
2180
|
+
const weightB = b.computeWeight || 0.5;
|
|
2181
|
+
return weightA - weightB;
|
|
2182
|
+
});
|
|
2183
|
+
// Select model based on tier (default to fast/cheapest)
|
|
2184
|
+
// Note: This should only run if initialTier is NOT specified (we return early above if it is)
|
|
2185
|
+
// But we still default to 'fast' here as a fallback
|
|
2186
|
+
let selectedModel;
|
|
2187
|
+
const tier = (initialTier || 'fast');
|
|
2188
|
+
switch (tier) {
|
|
2189
|
+
case 'fast':
|
|
2190
|
+
selectedModel = sorted[0]; // First item = lowest weight = cheapest
|
|
2191
|
+
break;
|
|
2192
|
+
case 'powerful':
|
|
2193
|
+
selectedModel = sorted[sorted.length - 1]; // Last item = highest weight = most powerful
|
|
2194
|
+
break;
|
|
2195
|
+
case 'balanced':
|
|
2196
|
+
default:
|
|
2197
|
+
// Middle model (rounded down)
|
|
2198
|
+
selectedModel = sorted[Math.floor(sorted.length / 2)];
|
|
2199
|
+
break;
|
|
2200
|
+
}
|
|
2201
|
+
if (selectedModel) {
|
|
2202
|
+
setCurrentModel({
|
|
2203
|
+
provider: initialProvider,
|
|
2204
|
+
model: selectedModel.id,
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}, [fetchedModels, initialProvider, initialTier, effectiveInitialModel, currentModel.model]);
|
|
1950
2211
|
// Switch model function - just updates local state
|
|
1951
2212
|
const switchModel = useCallback((model, provider) => {
|
|
1952
2213
|
const newProvider = provider || currentModel.provider;
|
|
@@ -1955,6 +2216,8 @@ function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialMod
|
|
|
1955
2216
|
provider: newProvider,
|
|
1956
2217
|
model: model,
|
|
1957
2218
|
});
|
|
2219
|
+
// Mark that user has made an explicit selection
|
|
2220
|
+
setHasUserSwitched(true);
|
|
1958
2221
|
// Update usage stats
|
|
1959
2222
|
setUsageStats(prev => ({
|
|
1960
2223
|
...prev,
|
|
@@ -2009,11 +2272,17 @@ function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialMod
|
|
|
2009
2272
|
};
|
|
2010
2273
|
}, [currentModel, availableModels]);
|
|
2011
2274
|
// Get current model info for streaming API
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2275
|
+
// Returns null if model is empty (tier-based selection is being used)
|
|
2276
|
+
const getCurrentModelInfo = useCallback(() => {
|
|
2277
|
+
if (!currentModel.model) {
|
|
2278
|
+
return null; // Empty model means tier-based selection is active
|
|
2279
|
+
}
|
|
2280
|
+
return {
|
|
2281
|
+
provider: currentModel.provider,
|
|
2282
|
+
model: currentModel.model,
|
|
2283
|
+
capabilities: getCurrentCapabilities(),
|
|
2284
|
+
};
|
|
2285
|
+
}, [currentModel, getCurrentCapabilities]);
|
|
2017
2286
|
// Flatten models for backward compatibility
|
|
2018
2287
|
const models = Object.values(availableModels).flat();
|
|
2019
2288
|
const modelsByProvider = availableModels;
|
|
@@ -2025,8 +2294,8 @@ function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialMod
|
|
|
2025
2294
|
currentModel,
|
|
2026
2295
|
currentCapabilities,
|
|
2027
2296
|
usageStats,
|
|
2028
|
-
isLoading:
|
|
2029
|
-
error:
|
|
2297
|
+
isLoading: isLoadingModels,
|
|
2298
|
+
error: modelsError,
|
|
2030
2299
|
// Actions
|
|
2031
2300
|
switchModel,
|
|
2032
2301
|
trackUsage,
|
|
@@ -2068,6 +2337,25 @@ theme, onModelSelectionChange, }) {
|
|
|
2068
2337
|
initialProvider: defaultModel?.provider,
|
|
2069
2338
|
initialModel: defaultModel?.model,
|
|
2070
2339
|
});
|
|
2340
|
+
// Helper: compute a display-only model object when provider/model mismatch exists (no state writes to avoid loops)
|
|
2341
|
+
const getDisplayModelForHeader = () => {
|
|
2342
|
+
if (!currentModel)
|
|
2343
|
+
return null;
|
|
2344
|
+
const provider = (currentModel.provider || '').toLowerCase();
|
|
2345
|
+
const modelId = (currentModel.model || '').toLowerCase();
|
|
2346
|
+
const allModels = Object.values(modelsByProvider || {}).flat();
|
|
2347
|
+
const exact = allModels.find((m) => m.id.toLowerCase() === modelId && (m.provider || '').toLowerCase() === provider);
|
|
2348
|
+
if (exact)
|
|
2349
|
+
return exact;
|
|
2350
|
+
// Fallback: pick a sensible default for the provider without mutating state
|
|
2351
|
+
const providerModels = (modelsByProvider && modelsByProvider[provider] && modelsByProvider[provider].length > 0)
|
|
2352
|
+
? modelsByProvider[provider]
|
|
2353
|
+
: DEFAULT_MODELS[provider] || [];
|
|
2354
|
+
if (providerModels.length > 0) {
|
|
2355
|
+
return [...providerModels].sort((a, b) => (a.computeWeight || 0.5) - (b.computeWeight || 0.5))[0];
|
|
2356
|
+
}
|
|
2357
|
+
return null;
|
|
2358
|
+
};
|
|
2071
2359
|
const lastModelRef = useRef(null);
|
|
2072
2360
|
useEffect(() => {
|
|
2073
2361
|
if (onModelSelectionChange && currentModel && currentCapabilities) {
|
|
@@ -2205,10 +2493,11 @@ theme, onModelSelectionChange, }) {
|
|
|
2205
2493
|
const getCurrentModelName = () => {
|
|
2206
2494
|
if (!currentModel)
|
|
2207
2495
|
return 'AI Model';
|
|
2208
|
-
//
|
|
2209
|
-
const
|
|
2210
|
-
|
|
2211
|
-
|
|
2496
|
+
// Prefer exact match; otherwise, display a sensible provider default without mutating state
|
|
2497
|
+
const modelObj = getDisplayModelForHeader();
|
|
2498
|
+
if (modelObj)
|
|
2499
|
+
return modelObj.name || modelObj.id;
|
|
2500
|
+
return currentModel.model || 'AI Model';
|
|
2212
2501
|
};
|
|
2213
2502
|
// Get model-specific usage for tooltip
|
|
2214
2503
|
const getModelUsage = (modelId, provider) => {
|
|
@@ -2440,11 +2729,77 @@ additionalContext, }) {
|
|
|
2440
2729
|
const [attachedFiles, setAttachedFiles] = useState(externalAttachedFiles);
|
|
2441
2730
|
const [currentModelSelection, setCurrentModelSelection] = useState(externalCurrentModelSelection);
|
|
2442
2731
|
// Use the simplified model switcher hook
|
|
2443
|
-
|
|
2732
|
+
// If models not provided, it will auto-fetch from server using serverConfig
|
|
2733
|
+
// If defaultModel only has provider (no model), it will auto-select from specified tier (defaults to 'fast')
|
|
2734
|
+
const { trackUsage, getCurrentModelInfo } = useModelSwitcher({
|
|
2444
2735
|
customModels: models,
|
|
2445
2736
|
initialProvider: defaultModel?.provider,
|
|
2446
|
-
initialModel: defaultModel?.model,
|
|
2737
|
+
initialModel: defaultModel?.model, // Can be undefined to auto-select from tier
|
|
2738
|
+
initialTier: defaultModel?.tier, // Pass tier for auto-selection
|
|
2739
|
+
serverConfig: serverConfig, // Pass serverConfig for auto-fetching models
|
|
2447
2740
|
});
|
|
2741
|
+
// Get current model info with tier if available
|
|
2742
|
+
// CRITICAL: If defaultModel has tier, ALWAYS use tier for server-side selection
|
|
2743
|
+
// This ensures tier-based selection even if hook has auto-selected a model
|
|
2744
|
+
const getModelSelection = useCallback(() => {
|
|
2745
|
+
// Helper to ensure provider/model consistency at the last moment
|
|
2746
|
+
const belongsToProvider = (prov, m) => {
|
|
2747
|
+
if (!m)
|
|
2748
|
+
return true;
|
|
2749
|
+
const id = m.toLowerCase();
|
|
2750
|
+
const p = (prov || '').toLowerCase();
|
|
2751
|
+
if (p === 'gemini' || p === 'google')
|
|
2752
|
+
return id.startsWith('gemini');
|
|
2753
|
+
if (p === 'claude' || p === 'anthropic')
|
|
2754
|
+
return id.startsWith('claude');
|
|
2755
|
+
if (p === 'openai')
|
|
2756
|
+
return id.startsWith('gpt');
|
|
2757
|
+
return true;
|
|
2758
|
+
};
|
|
2759
|
+
// Priority 1: If defaultModel specifies a tier, ALWAYS use tier for server-side selection
|
|
2760
|
+
// This overrides ANY model selection (auto-selected or user-selected) to ensure tier is respected
|
|
2761
|
+
if (defaultModel?.tier && !defaultModel?.model) {
|
|
2762
|
+
const provider = currentModelSelection?.provider || defaultModel.provider || 'claude';
|
|
2763
|
+
const selection = {
|
|
2764
|
+
provider,
|
|
2765
|
+
tier: defaultModel.tier,
|
|
2766
|
+
};
|
|
2767
|
+
return selection;
|
|
2768
|
+
}
|
|
2769
|
+
// Priority 2: If currentModelSelection exists with an explicit model, use it
|
|
2770
|
+
if (currentModelSelection && currentModelSelection.model) {
|
|
2771
|
+
let selection = {
|
|
2772
|
+
provider: currentModelSelection.provider,
|
|
2773
|
+
model: currentModelSelection.model,
|
|
2774
|
+
};
|
|
2775
|
+
// Final guard: if model doesn't belong to provider, drop model so server picks correctly
|
|
2776
|
+
if (!belongsToProvider(selection.provider, selection.model)) {
|
|
2777
|
+
selection = { provider: selection.provider };
|
|
2778
|
+
}
|
|
2779
|
+
return selection;
|
|
2780
|
+
}
|
|
2781
|
+
// Priority 3: Use getCurrentModelInfo from hook
|
|
2782
|
+
const modelInfo = getCurrentModelInfo();
|
|
2783
|
+
if (modelInfo && modelInfo.model) {
|
|
2784
|
+
const selection = {
|
|
2785
|
+
provider: modelInfo.provider,
|
|
2786
|
+
model: modelInfo.model,
|
|
2787
|
+
};
|
|
2788
|
+
return selection;
|
|
2789
|
+
}
|
|
2790
|
+
// Priority 4: Fallback - use defaultModel tier/provider if available
|
|
2791
|
+
if (defaultModel?.provider) {
|
|
2792
|
+
const selection = {
|
|
2793
|
+
provider: defaultModel.provider,
|
|
2794
|
+
...(defaultModel.tier ? { tier: defaultModel.tier } : {}),
|
|
2795
|
+
...(defaultModel.model ? { model: defaultModel.model } : {}),
|
|
2796
|
+
};
|
|
2797
|
+
return selection;
|
|
2798
|
+
}
|
|
2799
|
+
console.warn('[Conversation] ⚠️ No model selection available - returning undefined');
|
|
2800
|
+
console.warn('[Conversation] ⚠️ This will cause the server to use DEFAULT_CONFIG.model fallback');
|
|
2801
|
+
return undefined;
|
|
2802
|
+
}, [currentModelSelection, defaultModel, getCurrentModelInfo]);
|
|
2448
2803
|
// Use streaming AI hook for AI functionality
|
|
2449
2804
|
const streamingAI = useStreamingAI({
|
|
2450
2805
|
userId,
|
|
@@ -2454,15 +2809,46 @@ additionalContext, }) {
|
|
|
2454
2809
|
setChats: externalSetChats,
|
|
2455
2810
|
currentChatId: externalCurrentChatId,
|
|
2456
2811
|
setCurrentChatId: externalSetCurrentChatId,
|
|
2457
|
-
modelSelection:
|
|
2812
|
+
modelSelection: getModelSelection(),
|
|
2458
2813
|
onUsageTracked: trackUsage,
|
|
2459
2814
|
chatLevel,
|
|
2460
2815
|
additionalContext,
|
|
2461
2816
|
});
|
|
2462
2817
|
// Handle model selection changes
|
|
2463
2818
|
const handleModelSelectionChange = useCallback((modelInfo) => {
|
|
2464
|
-
|
|
2465
|
-
|
|
2819
|
+
// Normalize provider/model pair to prevent mismatches (e.g., openai + claude-haiku)
|
|
2820
|
+
const provider = (modelInfo.provider || '').toLowerCase();
|
|
2821
|
+
const modelId = (modelInfo.model || '').toLowerCase();
|
|
2822
|
+
const belongsToProvider = (prov, m) => {
|
|
2823
|
+
if (!m)
|
|
2824
|
+
return true;
|
|
2825
|
+
if (prov === 'gemini' || prov === 'google')
|
|
2826
|
+
return m.startsWith('gemini');
|
|
2827
|
+
if (prov === 'claude' || prov === 'anthropic')
|
|
2828
|
+
return m.startsWith('claude');
|
|
2829
|
+
if (prov === 'openai')
|
|
2830
|
+
return m.startsWith('gpt');
|
|
2831
|
+
return true;
|
|
2832
|
+
};
|
|
2833
|
+
let normalized = modelInfo;
|
|
2834
|
+
if (!belongsToProvider(provider, modelId)) {
|
|
2835
|
+
// Pick a sensible default for the selected provider: lowest computeWeight
|
|
2836
|
+
const providerModels = DEFAULT_MODELS[provider] || [];
|
|
2837
|
+
const selected = [...providerModels].sort((a, b) => (a.computeWeight || 0.5) - (b.computeWeight || 0.5))[0];
|
|
2838
|
+
if (selected) {
|
|
2839
|
+
normalized = {
|
|
2840
|
+
provider: provider,
|
|
2841
|
+
model: selected.id,
|
|
2842
|
+
capabilities: { supportsImages: selected.supportsImages || false, computeWeight: selected.computeWeight || 1.0 },
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
else {
|
|
2846
|
+
// If no known models, drop to provider with same model string (will be corrected later in request layer)
|
|
2847
|
+
normalized = { ...modelInfo, model: '' };
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
setCurrentModelSelection(normalized);
|
|
2851
|
+
externalOnModelSelectionChange?.(normalized);
|
|
2466
2852
|
}, [externalOnModelSelectionChange]);
|
|
2467
2853
|
// Handle sending messages
|
|
2468
2854
|
const handleSend = useCallback((additionalContext, queryToSend) => {
|
|
@@ -3407,5 +3793,5 @@ function useAiMerge({ ai, draft, onMerged, config }) {
|
|
|
3407
3793
|
return { pending, applyPending, clearPending };
|
|
3408
3794
|
}
|
|
3409
3795
|
|
|
3410
|
-
export { ChatPanel, Icon as CommandIcon, Conversation, ErrorBoundary, ModelSwitcher, SuggestionCard, SuggestionsPanel, defaultTheme as defaultCommandTheme, mergeWithAi, useAiMerge, useDebouncedSuggestions, useModelSwitcher, useStreamingAI, useSuggestions };
|
|
3796
|
+
export { ChatPanel, Icon as CommandIcon, Conversation, ErrorBoundary, ModelSwitcher, SuggestionCard, SuggestionsPanel, defaultTheme as defaultCommandTheme, mergeWithAi, useAiMerge, useDebouncedSuggestions, useModelSwitcher, useResolvedDefaultModel, useStreamingAI, useSuggestions };
|
|
3411
3797
|
//# sourceMappingURL=index.esm.js.map
|