orchid-ai 1.2.3 → 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.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-20250514',
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-opus-4-20250514',
44
- name: 'Claude Opus 4',
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: 1.2,
48
+ computeWeight: 0.2,
49
49
  },
50
50
  {
51
- id: 'claude-sonnet-4-20250514',
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-3-5-haiku-20241022',
60
- name: 'Claude Haiku 3.5',
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: 0.2,
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: 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
- function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialModel = DEFAULT_CONFIG.model, customModels, } = {}) {
1931
- // Use default models or custom models
1932
- const availableModels = customModels || DEFAULT_MODELS;
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: initialModel,
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: initialModel,
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
- const getCurrentModelInfo = useCallback(() => ({
2013
- provider: currentModel.provider,
2014
- model: currentModel.model,
2015
- capabilities: getCurrentCapabilities(),
2016
- }), [currentModel, getCurrentCapabilities]);
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: false, // No loading since no API calls
2029
- error: null,
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
- // Find the actual model object to get the name
2209
- const allModels = Object.values(modelsByProvider).flat();
2210
- const modelObj = allModels.find((m) => m.id === currentModel.model && m.provider === currentModel.provider);
2211
- return modelObj?.name || currentModel.model;
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
- const { trackUsage } = useModelSwitcher({
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: currentModelSelection,
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
- setCurrentModelSelection(modelInfo);
2465
- externalOnModelSelectionChange?.(modelInfo);
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