diff --git a/packages/ai/src/providers/_ProviderRepository.ts b/packages/ai/src/providers/_ProviderRepository.ts index 9733fe3..0e5c30d 100644 --- a/packages/ai/src/providers/_ProviderRepository.ts +++ b/packages/ai/src/providers/_ProviderRepository.ts @@ -24,10 +24,68 @@ export class ProviderRepository { }; static async getModelFamily(model: any, env: GenericEnv) { + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Looking up model "${model}"`); + const allModels = await env.KV_STORAGE.get('supportedModels'); const models = JSON.parse(allModels); - const modelData = models.filter((m: ModelMeta) => m.id === model); - return modelData[0].provider; + + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Found ${models.length} total models in KV storage`); + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.getModelFamily: Available model IDs:', models.map((m: ModelMeta) => m.id)); + + // First try exact match + let modelData = models.filter((m: ModelMeta) => m.id === model); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Exact match attempt for "${model}" found ${modelData.length} results`); + + // If no exact match, try to find by partial match (handle provider prefixes) + if (modelData.length === 0) { + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Trying partial match for "${model}"`); + modelData = models.filter((m: ModelMeta) => { + // Check if the model ID ends with the requested model name + // This handles cases like "accounts/fireworks/models/mixtral-8x22b-instruct" matching "mixtral-8x22b-instruct" + const endsWithMatch = m.id.endsWith(model); + const modelEndsWithStoredBase = model.endsWith(m.id.split('/').pop() || ''); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Checking "${m.id}" - endsWith: ${endsWithMatch}, modelEndsWithBase: ${modelEndsWithStoredBase}`); + return endsWithMatch || modelEndsWithStoredBase; + }); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Partial match found ${modelData.length} results`); + } + + // If still no match, try to find by the base model name (last part after /) + if (modelData.length === 0) { + const baseModelName = model.split('/').pop(); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Trying base name match for "${baseModelName}"`); + modelData = models.filter((m: ModelMeta) => { + const baseStoredName = m.id.split('/').pop(); + const matches = baseStoredName === baseModelName; + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Comparing base names "${baseStoredName}" === "${baseModelName}": ${matches}`); + return matches; + }); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Base name match found ${modelData.length} results`); + } + + const selectedProvider = modelData[0]?.provider; + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: Final result for "${model}" -> provider: "${selectedProvider}"`); + + if (modelData.length > 0) { + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.getModelFamily: Selected model data:', modelData[0]); + } else { + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.getModelFamily: No matching model found for "${model}"`); + } + + return selectedProvider; } static async getModelMeta(meta: any, env: GenericEnv) { @@ -41,12 +99,19 @@ export class ProviderRepository { } setProviders(env: GenericEnv) { + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.setProviders: Starting provider detection'); + const indicies = { providerName: 0, providerValue: 1, }; const valueDelimiter = '_'; const envKeys = Object.keys(env); + + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.setProviders: Environment keys ending with KEY:', envKeys.filter(key => key.endsWith('KEY'))); + for (let i = 0; i < envKeys.length; i++) { if (envKeys.at(i)?.endsWith('KEY')) { const detectedProvider = envKeys @@ -55,9 +120,15 @@ export class ProviderRepository { .at(indicies.providerName) ?.toLowerCase(); const detectedProviderValue = env[envKeys.at(i) as string]; + + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Processing ${envKeys[i]} -> detected provider: "${detectedProvider}", has value: ${!!detectedProviderValue}`); + if (detectedProviderValue) { switch (detectedProvider) { case 'anthropic': + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Claude provider (anthropic)'); this.#providers.push({ name: 'claude', key: env.ANTHROPIC_API_KEY, @@ -65,6 +136,8 @@ export class ProviderRepository { }); break; case 'gemini': + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Google provider (gemini)'); this.#providers.push({ name: 'google', key: env.GEMINI_API_KEY, @@ -72,6 +145,8 @@ export class ProviderRepository { }); break; case 'cloudflare': + // eslint-disable-next-line prettier/prettier + console.log('[DEBUG_LOG] ProviderRepository.setProviders: Adding Cloudflare provider'); this.#providers.push({ name: 'cloudflare', key: env.CLOUDFLARE_API_KEY, @@ -82,6 +157,8 @@ export class ProviderRepository { }); break; default: + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Adding default provider "${detectedProvider}"`); this.#providers.push({ name: detectedProvider as SupportedProvider, key: env[envKeys[i] as string], @@ -89,8 +166,14 @@ export class ProviderRepository { ProviderRepository.OPENAI_COMPAT_ENDPOINTS[detectedProvider as SupportedProvider], }); } + } else { + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Skipping ${envKeys[i]} - no value provided`); } } } + + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ProviderRepository.setProviders: Final configured providers (${this.#providers.length}):`, this.#providers.map(p => ({ name: p.name, endpoint: p.endpoint, hasKey: !!p.key }))); } } diff --git a/packages/ai/src/providers/chat-stream-provider.ts b/packages/ai/src/providers/chat-stream-provider.ts index 95667d2..d40aa8a 100644 --- a/packages/ai/src/providers/chat-stream-provider.ts +++ b/packages/ai/src/providers/chat-stream-provider.ts @@ -236,7 +236,7 @@ export abstract class BaseChatProvider implements ChatStreamProvider { // Process chunk normally for non-tool-call responses if (!chunk.choices[0]?.delta?.tool_calls) { - console.log('after-tool-call-chunk', chunk); + // console.log('after-tool-call-chunk', chunk); const shouldBreak = await this.processChunk(chunk, dataCallback); if (shouldBreak) { conversationComplete = true; diff --git a/packages/ai/src/providers/fireworks.ts b/packages/ai/src/providers/fireworks.ts index 89d6db9..58d4ccb 100644 --- a/packages/ai/src/providers/fireworks.ts +++ b/packages/ai/src/providers/fireworks.ts @@ -18,7 +18,7 @@ export class FireworksAiChatProvider extends BaseChatProvider { } return { - model: `${modelPrefix}${param.model}`, + model: `${param.model}`, messages: safeMessages, stream: true, }; diff --git a/packages/ai/src/tools/maps.ts b/packages/ai/src/tools/maps.ts index 4c60d42..c34a5bd 100644 --- a/packages/ai/src/tools/maps.ts +++ b/packages/ai/src/tools/maps.ts @@ -9,14 +9,14 @@ export interface MapsControlResult { */ export const MapsTools = { type: 'function', - description: - 'Interface for controlling a web-rendered map to explore publicly available geospatial data', /** * Mock implementation of a maps control command. */ function: { name: 'maps_control', + description: + 'Interface for controlling a web-rendered map to explore publicly available geospatial data', parameters: { type: 'object', properties: { diff --git a/packages/client/src/components/chat/input-menu/InputMenu.tsx b/packages/client/src/components/chat/input-menu/InputMenu.tsx index 0f442d4..064909a 100644 --- a/packages/client/src/components/chat/input-menu/InputMenu.tsx +++ b/packages/client/src/components/chat/input-menu/InputMenu.tsx @@ -8,6 +8,7 @@ import { MenuButton, MenuItem, MenuList, + Spinner, Text, useDisclosure, useOutsideClick, @@ -41,19 +42,38 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled }) const [controlledOpen, setControlledOpen] = useState(false); const [supportedModels, setSupportedModels] = useState([]); + const [isLoadingModels, setIsLoadingModels] = useState(true); useEffect(() => { setControlledOpen(isOpen); }, [isOpen]); useEffect(() => { + setIsLoadingModels(true); fetch('/api/models') .then(response => response.json()) .then(models => { setSupportedModels(models); + + // Update the ModelStore with supported models + const modelIds = models.map((model: any) => model.id); + clientChatStore.setSupportedModels(modelIds); + + // If no model is currently selected or the current model is not in the list, + // select a random model from the available ones + if (!clientChatStore.model || !modelIds.includes(clientChatStore.model)) { + if (models.length > 0) { + const randomIndex = Math.floor(Math.random() * models.length); + const randomModel = models[randomIndex]; + clientChatStore.setModel(randomModel.id); + } + } + + setIsLoadingModels(false); }) .catch(err => { console.error('Could not fetch models: ', err); + setIsLoadingModels(false); }); }, []); @@ -108,8 +128,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled }) } - isDisabled={isDisabled} + icon={isLoadingModels ? : } + isDisabled={isDisabled || isLoadingModels} aria-label="Settings" _hover={{ bg: 'rgba(255, 255, 255, 0.2)' }} _focus={{ boxShadow: 'none' }} @@ -118,8 +138,8 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled }) ) : ( } - isDisabled={isDisabled} + rightIcon={isLoadingModels ? : } + isDisabled={isDisabled || isLoadingModels} variant="ghost" display="flex" justifyContent="space-between" @@ -128,7 +148,7 @@ const InputMenu: React.FC<{ isDisabled?: boolean }> = observer(({ isDisabled }) {...MsM_commonButtonStyles} > - {clientChatStore.model} + {isLoadingModels ? 'Loading...' : clientChatStore.model} )} diff --git a/packages/client/src/components/landing-component/LandingComponent.tsx b/packages/client/src/components/landing-component/LandingComponent.tsx index 78acd08..14e2a05 100644 --- a/packages/client/src/components/landing-component/LandingComponent.tsx +++ b/packages/client/src/components/landing-component/LandingComponent.tsx @@ -1,5 +1,5 @@ import { Box } from '@chakra-ui/react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; import { useComponent } from '../contexts/ComponentContext.tsx'; @@ -11,6 +11,23 @@ export const LandingComponent: React.FC = () => { const [mapActive, setMapActive] = useState(true); const [aiActive, setAiActive] = useState(false); + const appCtlState = `app-ctl-state`; + + useLayoutEffect(() => { + const value = localStorage.getItem(appCtlState); + if (value) { + const parsed = JSON.parse(value); + setIntensity(parsed.intensity); + setMapActive(parsed.mapActive); + setAiActive(parsed.aiActive); + } + }, []); + + // create a hook for saving the state as a json object when it changes + useEffect(() => { + localStorage.setItem(appCtlState, JSON.stringify({ intensity, mapActive, aiActive })); + }); + const component = useComponent(); const { setEnabledComponent } = component; @@ -21,12 +38,14 @@ export const LandingComponent: React.FC = () => { if (aiActive) { setEnabledComponent('ai'); } - }, []); + }, [mapActive, aiActive, setEnabledComponent]); return ( { }} /> - {/**/} ); }; diff --git a/packages/client/src/components/landing-component/Map.tsx b/packages/client/src/components/landing-component/Map.tsx index b917532..b1d0d4e 100644 --- a/packages/client/src/components/landing-component/Map.tsx +++ b/packages/client/src/components/landing-component/Map.tsx @@ -37,10 +37,10 @@ const key = function Map(props: { visible: boolean }) { return ( /* Full-screen wrapper — fills the viewport and becomes the positioning context */ - + {/* Button bar — absolutely positioned inside the wrapper */} - + {/*(null); useEffect(() => { setAuthenticated(true); setIsTokenLoading(false); }, []); - const [mapView, setMapView] = useState({ - longitude: -122.4, - latitude: 37.8, - zoom: 14, - }); + // Handle map resize when component becomes visible + useEffect(() => { + if (props.visible && mapRef.current) { + // Small delay to ensure the container is fully visible + const timer = setTimeout(() => { + if (mapRef.current) { + mapRef.current.resize(); + } + }, 100); + + return () => clearTimeout(timer); + } + }, [props.visible]); const handleNavigationClick = useCallback(async () => { console.log('handling navigation in map'); @@ -39,7 +51,10 @@ export default function MapNext(props: any = { mapboxPublicKey: '' } as any) { const handleMapViewChange = useCallback(async (evt: any) => { const { longitude, latitude, zoom } = evt.viewState; - setMapView({ longitude, latitude, zoom }); + clientChatStore.setMapView(longitude, latitude, zoom); + // setMapView({ longitude, latitude, zoom }); + + // Update the store with the new view state }, []); const pins = useMemo( @@ -98,13 +113,15 @@ Type '{ city: string; population: string; image: string; state: string; latitude {/* */} {/**/} ); } + +const MapNext = observer(MapNextComponent); +export default MapNext; diff --git a/packages/client/src/components/landing-component/Tweakbox.tsx b/packages/client/src/components/landing-component/Tweakbox.tsx index ee6d4d5..9fd60b8 100644 --- a/packages/client/src/components/landing-component/Tweakbox.tsx +++ b/packages/client/src/components/landing-component/Tweakbox.tsx @@ -14,7 +14,7 @@ import { } from '@chakra-ui/react'; import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; import { observer } from 'mobx-react-lite'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; interface SliderControl { value: number; @@ -34,6 +34,8 @@ interface SwitchControl { } interface TweakboxProps { + id: string; + persist: boolean; sliders: { speed: SliderControl; intensity: SliderControl; @@ -44,7 +46,7 @@ interface TweakboxProps { } & Record; } -const Tweakbox = observer(({ sliders, switches }: TweakboxProps) => { +const Tweakbox = observer(({ id, persist, sliders, switches }: TweakboxProps) => { const [isCollapsed, setIsCollapsed] = useState(false); return ( diff --git a/packages/client/src/pages/index/+Page.tsx b/packages/client/src/pages/index/+Page.tsx index d5fc162..4e277eb 100644 --- a/packages/client/src/pages/index/+Page.tsx +++ b/packages/client/src/pages/index/+Page.tsx @@ -1,4 +1,4 @@ -import { Box } from '@chakra-ui/react'; +import { Box, useMediaQuery } from '@chakra-ui/react'; import React, { useEffect } from 'react'; import Chat from '../../components/chat/Chat.tsx'; @@ -21,10 +21,11 @@ export default function IndexPage() { const component = useComponent(); + const mediaQuery = useMediaQuery(); + return ( - diff --git a/packages/client/src/stores/ClientChatStore.ts b/packages/client/src/stores/ClientChatStore.ts index 484043b..9937c96 100644 --- a/packages/client/src/stores/ClientChatStore.ts +++ b/packages/client/src/stores/ClientChatStore.ts @@ -3,13 +3,14 @@ // --------------------------- import { types, type Instance } from 'mobx-state-tree'; +import { MapStore } from './MapStore'; import { MessagesStore } from './MessagesStore'; import { ModelStore } from './ModelStore'; import { StreamStore } from './StreamStore'; import { UIStore } from './UIStore'; export const ClientChatStore = types - .compose(MessagesStore, UIStore, ModelStore, StreamStore) + .compose(MessagesStore, UIStore, ModelStore, StreamStore, MapStore) .named('ClientChatStore'); const clientChatStore = ClientChatStore.create(); diff --git a/packages/client/src/stores/MapStore.ts b/packages/client/src/stores/MapStore.ts new file mode 100644 index 0000000..78d9f5d --- /dev/null +++ b/packages/client/src/stores/MapStore.ts @@ -0,0 +1,117 @@ +import { types, type Instance } from 'mobx-state-tree'; + +export interface MapControlCommand { + action: string; + value?: string; + data?: any; +} + +export const MapStore = types + .model('MapStore', { + // Current map view state + longitude: types.optional(types.number, -122.4), + latitude: types.optional(types.number, 37.8), + zoom: types.optional(types.number, 14), + // Map control state + isControlActive: types.optional(types.boolean, false), + }) + .volatile(self => ({ + // Store pending map commands from AI + pendingCommands: [] as MapControlCommand[], + mapState: { + latitude: self.latitude, + longitude: self.longitude, + zoom: self.zoom, + bearing: 0, + pitch: 0, + } as any, + })) + .actions(self => ({ + // Update map view state + setMapView(longitude: number, latitude: number, zoom: number) { + self.longitude = longitude; + self.latitude = latitude; + self.zoom = zoom; + + // Also update the mapState object to keep it in sync + self.mapState = { + ...self.mapState, + longitude, + latitude, + zoom, + }; + }, + + // Handle map control commands from AI + executeMapCommand(command: MapControlCommand) { + console.log('[DEBUG_LOG] Executing map command:', command); + + switch (command.action) { + case 'zoom_to': { + if (command.data?.target) { + // For now, we'll implement a simple zoom behavior + // In a real implementation, this could parse coordinates or location names + const zoomLevel = 10; // Default zoom level for zoom_to commands + self.zoom = zoomLevel; + console.log('[DEBUG_LOG] Zoomed to level:', zoomLevel); + } + break; + } + + case 'add_point': { + if (command.data?.pointId) { + console.log('[DEBUG_LOG] Adding point:', command.data.pointId); + // Point addition logic would go here + } + break; + } + + case 'add_dataset': + case 'remove_dataset': { + if (command.data?.datasetId) { + console.log('[DEBUG_LOG] Dataset operation:', command.action, command.data.datasetId); + // Dataset management logic would go here + } + break; + } + + case 'search_datasets': { + console.log('[DEBUG_LOG] Searching datasets:', command.data?.searchTerm); + // Dataset search logic would go here + break; + } + + default: + console.warn('[DEBUG_LOG] Unknown map command:', command.action); + } + + self.isControlActive = true; + + // Clear the command after a short delay + setTimeout(() => { + self.isControlActive = false; + }, 1000); + }, + + // Add a command to the pending queue + addPendingCommand(command: MapControlCommand) { + self.pendingCommands.push(command); + }, + + // Process all pending commands + processPendingCommands() { + while (self.pendingCommands.length > 0) { + const command = self.pendingCommands.shift(); + if (command) { + this.executeMapCommand(command); + } + } + }, + + // Clear all pending commands + clearPendingCommands() { + self.pendingCommands.splice(0); + }, + })); + +export type IMapStore = Instance; diff --git a/packages/client/src/stores/StreamStore.ts b/packages/client/src/stores/StreamStore.ts index 58db0b6..1d6b396 100644 --- a/packages/client/src/stores/StreamStore.ts +++ b/packages/client/src/stores/StreamStore.ts @@ -2,6 +2,8 @@ import { flow, getParent, type Instance, types } from 'mobx-state-tree'; import Message, { batchContentUpdate } from '../models/Message'; +import clientChatStore from './ClientChatStore.ts'; +import type { MapControlCommand } from './MapStore'; import type { RootDeps } from './RootDeps.ts'; import UserOptionsStore from './UserOptionsStore'; @@ -92,6 +94,30 @@ export const StreamStore = types return; } + // Handle tool call responses + if (parsed.type === 'tool_result') { + console.log('[DEBUG_LOG] Received tool result:', parsed); + + // Check if this is a map control tool call + if (parsed.tool_name === 'maps_control' && parsed.result?.data) { + const mapCommand: MapControlCommand = { + action: parsed.result.data.action, + value: parsed.args?.value, + data: parsed.result.data, + }; + + console.log('[DEBUG_LOG] Processing map command:', mapCommand); + + // Execute the map command through the store + if ('executeMapCommand' in root) { + (root as any).executeMapCommand(mapCommand); + } else { + console.warn('[DEBUG_LOG] MapStore not available in root'); + } + } + return; + } + // Get the last message const lastMessage = root.items[root.items.length - 1]; @@ -152,4 +178,4 @@ export const StreamStore = types return { sendMessage, stopIncomingMessage, cleanup, setEventSource, setStreamId }; }); -export interface IStreamStore extends Instance {} +export type IStreamStore = Instance; diff --git a/packages/services/src/chat-service/ChatService.ts b/packages/services/src/chat-service/ChatService.ts index d210911..dbedc0e 100644 --- a/packages/services/src/chat-service/ChatService.ts +++ b/packages/services/src/chat-service/ChatService.ts @@ -183,7 +183,7 @@ const ChatService = types modelMeta.set(mdl.id, { ...mdl, ...meta }); } catch (err) { // logger.error(`Metadata fetch failed for ${mdl.id}`, err); - modelMeta.set(mdl.id, { provider: provider.name, mdl }); + modelMeta.set(mdl.id, { provider: provider.name, ...mdl }); } } } catch (err) { @@ -277,8 +277,23 @@ const ChatService = types }) { const { streamConfig, streamParams, controller, encoder, streamId } = params; + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Processing model "${streamConfig.model}" for stream ${streamId}`, + ); + const modelFamily = await ProviderRepository.getModelFamily(streamConfig.model, self.env); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Detected model family "${modelFamily}" for model "${streamConfig.model}"`, + ); + // eslint-disable-next-line prettier/prettier + console.log( + '[DEBUG_LOG] ChatService.runModelHandler: Available model handlers:', + Object.keys(modelHandlers), + ); + const useModelHandler = () => { // @ts-expect-error - language server does not have enough information to validate modelFamily as an indexer for modelHandlers return modelHandlers[modelFamily]; @@ -287,9 +302,28 @@ const ChatService = types const handler = useModelHandler(); if (handler) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Found handler for model family "${modelFamily}"`, + ); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Calling handler for model "${streamConfig.model}" with maxTokens: ${streamParams.maxTokens}`, + ); + try { await handler(streamParams, Common.Utils.handleStreamData(controller, encoder)); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Successfully completed handler for model "${streamConfig.model}"`, + ); } catch (error: any) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Handler error for model "${streamConfig.model}":`, + error.message, + ); + const message = error.message.toLowerCase(); if ( @@ -318,10 +352,80 @@ const ChatService = types ); } if (message.includes('404')) { - throw new ClientError(`Something went wrong, try again.`, 413, {}); + // Try to find a fallback model from the same provider + try { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Model "${streamConfig.model}" not found, attempting fallback`, + ); + + const allModels = await self.env.KV_STORAGE.get('supportedModels'); + const models = JSON.parse(allModels); + + // Find all models from the same provider + const sameProviderModels = models.filter( + (m: any) => m.provider === modelFamily && m.id !== streamConfig.model, + ); + + if (sameProviderModels.length > 0) { + // Try the first available model from the same provider + const fallbackModel = sameProviderModels[0]; + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Trying fallback model "${fallbackModel.id}" from provider "${modelFamily}"`, + ); + + // Update streamParams with the fallback model + const fallbackStreamParams = { ...streamParams, model: fallbackModel.id }; + + // Try the fallback model + await handler( + fallbackStreamParams, + Common.Utils.handleStreamData(controller, encoder), + ); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Successfully completed handler with fallback model "${fallbackModel.id}"`, + ); + return; // Success with fallback + } else { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: No fallback models available for provider "${modelFamily}"`, + ); + } + } catch (fallbackError: any) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: Fallback attempt failed:`, + fallbackError.message, + ); + } + + throw new ClientError( + `Model not found or unavailable. Please try a different model.`, + 404, + { + model: streamConfig.model, + }, + ); } throw error; } + } else { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.runModelHandler: No handler found for model family "${modelFamily}" (model: "${streamConfig.model}")`, + ); + throw new ClientError( + `No handler available for model family "${modelFamily}". Model: "${streamConfig.model}"`, + 500, + { + model: streamConfig.model, + modelFamily: modelFamily, + availableHandlers: Object.keys(modelHandlers), + }, + ); } }, @@ -333,11 +437,27 @@ const ChatService = types }) { const { streamId, streamConfig, savedStreamConfig, durableObject } = params; + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.createSseReadableStream: Creating stream ${streamId} for model "${streamConfig.model}"`, + ); + // eslint-disable-next-line prettier/prettier + console.log(`[DEBUG_LOG] ChatService.createSseReadableStream: Stream config:`, { + model: streamConfig.model, + systemPrompt: streamConfig.systemPrompt?.substring(0, 100) + '...', + messageCount: streamConfig.messages?.length, + }); + return new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); try { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.createSseReadableStream: Starting stream processing for ${streamId}`, + ); + const dynamicContext = Schema.Message.create(streamConfig.preprocessedContext); // Process the stream data using the appropriate handler @@ -347,6 +467,16 @@ const ChatService = types durableObject, ); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.createSseReadableStream: Created stream params for ${streamId}:`, + { + model: streamParams.model, + maxTokens: streamParams.maxTokens, + messageCount: streamParams.messages?.length, + }, + ); + await self.runModelHandler({ streamConfig, streamParams, @@ -355,6 +485,11 @@ const ChatService = types streamId, }); } catch (error) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.createSseReadableStream: Error in stream ${streamId}:`, + error, + ); console.error(`chatService::handleSseStream::${streamId}::Error`, error); if (error instanceof ClientError) { @@ -376,6 +511,10 @@ const ChatService = types controller.close(); } finally { try { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.createSseReadableStream: Closing stream ${streamId}`, + ); controller.close(); } catch (_) { // Ignore errors when closing the controller, as it might already be closed @@ -388,21 +527,53 @@ const ChatService = types handleSseStream: flow(function* ( streamId: string, ): Generator, Response, unknown> { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Handling SSE stream request for ${streamId}`, + ); + // Check if a stream is already active for this ID if (self.activeStreams.has(streamId)) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Stream ${streamId} already active, returning 409`, + ); return new Response('Stream already active', { status: 409 }); } + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Retrieving stream configuration for ${streamId}`, + ); + // Retrieve the stream configuration from the durable object const objectId = self.env.SERVER_COORDINATOR.idFromName('stream-index'); const durableObject = self.env.SERVER_COORDINATOR.get(objectId); const savedStreamConfig: any = yield durableObject.getStreamData(streamId); if (!savedStreamConfig) { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: No stream configuration found for ${streamId}, returning 404`, + ); return new Response('Stream not found', { status: 404 }); } const streamConfig = JSON.parse(savedStreamConfig); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Retrieved stream config for ${streamId}:`, + { + model: streamConfig.model, + messageCount: streamConfig.messages?.length, + systemPrompt: streamConfig.systemPrompt?.substring(0, 100) + '...', + }, + ); + + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Creating SSE readable stream for ${streamId}`, + ); const stream = self.createSseReadableStream({ streamId, @@ -414,18 +585,37 @@ const ChatService = types // Use `tee()` to create two streams: one for processing and one for the response const [processingStream, responseStream] = stream.tee(); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Setting active stream for ${streamId}`, + ); + self.setActiveStream(streamId, { ...streamConfig, }); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Setting up processing stream pipeline for ${streamId}`, + ); + processingStream.pipeTo( new WritableStream({ close() { + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Processing stream closed for ${streamId}, removing active stream`, + ); self.removeActiveStream(streamId); }, }), ); + // eslint-disable-next-line prettier/prettier + console.log( + `[DEBUG_LOG] ChatService.handleSseStream: Returning response stream for ${streamId}`, + ); + // Return the second stream as the response return new Response(responseStream, { headers: {