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.
Files changed (38) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/components/ChatPanel.d.ts +2 -1
  3. package/dist/cli/components/Conversation.d.ts +2 -1
  4. package/dist/cli/components/ModelSwitcher.d.ts +2 -1
  5. package/dist/cli/hooks/useModelSwitcher.d.ts +7 -4
  6. package/dist/cli/hooks/useResolvedDefaultModel.d.ts +21 -0
  7. package/dist/cli/hooks/useStreamingAI.d.ts +3 -2
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/server/contextual-service.d.ts +49 -1
  10. package/dist/cli/server/intent-detection.d.ts +2 -0
  11. package/dist/cli/server/utils.d.ts +0 -12
  12. package/dist/cli/types/types.d.ts +2 -1
  13. package/dist/components/ChatPanel.d.ts +2 -1
  14. package/dist/components/Conversation.d.ts +2 -1
  15. package/dist/components/ModelSwitcher.d.ts +2 -1
  16. package/dist/hooks/useModelSwitcher.d.ts +7 -4
  17. package/dist/hooks/useResolvedDefaultModel.d.ts +21 -0
  18. package/dist/hooks/useStreamingAI.d.ts +3 -2
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.esm.js +425 -39
  21. package/dist/index.js +425 -38
  22. package/dist/server/components/ChatPanel.d.ts +2 -1
  23. package/dist/server/components/Conversation.d.ts +2 -1
  24. package/dist/server/components/ModelSwitcher.d.ts +2 -1
  25. package/dist/server/contextual-service.d.ts +49 -1
  26. package/dist/server/hooks/useModelSwitcher.d.ts +7 -4
  27. package/dist/server/hooks/useResolvedDefaultModel.d.ts +21 -0
  28. package/dist/server/hooks/useStreamingAI.d.ts +3 -2
  29. package/dist/server/index.esm.js +507 -151
  30. package/dist/server/index.js +473 -99
  31. package/dist/server/intent-detection.d.ts +2 -0
  32. package/dist/server/server/contextual-service.d.ts +49 -1
  33. package/dist/server/server/intent-detection.d.ts +2 -0
  34. package/dist/server/server/utils.d.ts +0 -12
  35. package/dist/server/types/types.d.ts +2 -1
  36. package/dist/server/utils.d.ts +0 -12
  37. package/dist/types/types.d.ts +2 -1
  38. 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-20250514',
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-opus-4-20250514',
46
- name: 'Claude Opus 4',
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: 1.2,
50
+ computeWeight: 0.2,
51
51
  },
52
52
  {
53
- id: 'claude-sonnet-4-20250514',
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-3-5-haiku-20241022',
62
- name: 'Claude Haiku 3.5',
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: 0.2,
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: 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
- function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialModel = DEFAULT_CONFIG.model, customModels, } = {}) {
1933
- // Use default models or custom models
1934
- const availableModels = customModels || DEFAULT_MODELS;
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: initialModel,
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: initialModel,
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
- const getCurrentModelInfo = React.useCallback(() => ({
2015
- provider: currentModel.provider,
2016
- model: currentModel.model,
2017
- capabilities: getCurrentCapabilities(),
2018
- }), [currentModel, getCurrentCapabilities]);
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: false, // No loading since no API calls
2031
- error: null,
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
- // Find the actual model object to get the name
2211
- const allModels = Object.values(modelsByProvider).flat();
2212
- const modelObj = allModels.find((m) => m.id === currentModel.model && m.provider === currentModel.provider);
2213
- return modelObj?.name || currentModel.model;
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
- const { trackUsage } = useModelSwitcher({
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: currentModelSelection,
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
- setCurrentModelSelection(modelInfo);
2467
- externalOnModelSelectionChange?.(modelInfo);
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
@@ -20,7 +20,8 @@ interface ChatPanelProps {
20
20
  models?: Record<string, ModelInfo[]>;
21
21
  defaultModel?: {
22
22
  provider: string;
23
- model: string;
23
+ model?: string;
24
+ tier?: 'fast' | 'balanced' | 'powerful';
24
25
  };
25
26
  showUsageStats?: boolean;
26
27
  maxFileSize?: string;
@@ -27,7 +27,8 @@ interface ConversationProps {
27
27
  models?: Record<string, ModelInfo[]>;
28
28
  defaultModel?: {
29
29
  provider: string;
30
- model: string;
30
+ model?: string;
31
+ tier?: 'fast' | 'balanced' | 'powerful';
31
32
  };
32
33
  showUsageStats?: boolean;
33
34
  maxFileSize?: string;
@@ -4,7 +4,8 @@ interface ModelSwitcherProps {
4
4
  models?: Record<string, ModelInfo[]>;
5
5
  defaultModel?: {
6
6
  provider: string;
7
- model: string;
7
+ model?: string;
8
+ tier?: 'fast' | 'balanced' | 'powerful';
8
9
  };
9
10
  showUsageStats?: boolean;
10
11
  theme?: ChatTheme;