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