From 1dfbabc1d16969ea7defe5f64644a6c99f574c1d Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 9 Apr 2025 19:11:27 +0000 Subject: [PATCH] try using a new ProviderOptions approach --- .../llm/providers/anthropic_service.ts | 66 +++-- src/services/llm/providers/ollama_service.ts | 143 +++++------ src/services/llm/providers/openai_service.ts | 60 +++-- .../llm/providers/provider_options.ts | 202 +++++++++++++++ src/services/llm/providers/providers.ts | 243 ++++++++++++++++++ 5 files changed, 597 insertions(+), 117 deletions(-) create mode 100644 src/services/llm/providers/provider_options.ts diff --git a/src/services/llm/providers/anthropic_service.ts b/src/services/llm/providers/anthropic_service.ts index 5b364f428..b4fe719f2 100644 --- a/src/services/llm/providers/anthropic_service.ts +++ b/src/services/llm/providers/anthropic_service.ts @@ -2,6 +2,9 @@ import options from '../../options.js'; import { BaseAIService } from '../base_ai_service.js'; import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; +import type { AnthropicOptions } from './provider_options.js'; +import { getAnthropicOptions } from './providers.js'; +import log from '../../log.js'; interface AnthropicMessage { role: string; @@ -22,42 +25,67 @@ export class AnthropicService extends BaseAIService { throw new Error('Anthropic service is not available. Check API key and AI settings.'); } - const apiKey = options.getOption('anthropicApiKey'); - const baseUrl = options.getOption('anthropicBaseUrl') || PROVIDER_CONSTANTS.ANTHROPIC.BASE_URL; - const model = opts.model || options.getOption('anthropicDefaultModel') || PROVIDER_CONSTANTS.ANTHROPIC.DEFAULT_MODEL; + // Get provider-specific options from the central provider manager + const providerOptions = getAnthropicOptions(opts); - const temperature = opts.temperature !== undefined - ? opts.temperature - : parseFloat(options.getOption('aiTemperature') || '0.7'); + // Log provider metadata if available + if (providerOptions.providerMetadata) { + log.info(`Using model ${providerOptions.model} from provider ${providerOptions.providerMetadata.provider}`); - const systemPrompt = this.getSystemPrompt(opts.systemPrompt || options.getOption('aiSystemPrompt')); + // Log capabilities if available + const capabilities = providerOptions.providerMetadata.capabilities; + if (capabilities) { + log.info(`Model capabilities: ${JSON.stringify(capabilities)}`); + } + } + + const systemPrompt = this.getSystemPrompt(providerOptions.systemPrompt || options.getOption('aiSystemPrompt')); // Format for Anthropic's API const formattedMessages = this.formatMessages(messages, systemPrompt); + // Store the formatted messages in the provider options for future reference + providerOptions.formattedMessages = formattedMessages; + try { // Ensure base URL doesn't already include '/v1' and build the complete endpoint - const cleanBaseUrl = baseUrl.replace(/\/+$/, '').replace(/\/v1$/, ''); + const cleanBaseUrl = providerOptions.baseUrl.replace(/\/+$/, '').replace(/\/v1$/, ''); const endpoint = `${cleanBaseUrl}/v1/messages`; console.log(`Anthropic API endpoint: ${endpoint}`); - console.log(`Using model: ${model}`); + console.log(`Using model: ${providerOptions.model}`); + + // Create request body directly from provider options + const requestBody: any = { + model: providerOptions.model, + messages: formattedMessages.messages, + system: formattedMessages.system, + }; + + // Extract API parameters from provider options + const apiParams = { + temperature: providerOptions.temperature, + max_tokens: providerOptions.max_tokens, + stream: providerOptions.stream, + top_p: providerOptions.top_p + }; + + // Merge API parameters, filtering out undefined values + Object.entries(apiParams).forEach(([key, value]) => { + if (value !== undefined) { + requestBody[key] = value; + } + }); const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-Api-Key': apiKey, - 'anthropic-version': PROVIDER_CONSTANTS.ANTHROPIC.API_VERSION, - 'anthropic-beta': PROVIDER_CONSTANTS.ANTHROPIC.BETA_VERSION + 'X-Api-Key': providerOptions.apiKey, + 'anthropic-version': providerOptions.apiVersion || PROVIDER_CONSTANTS.ANTHROPIC.API_VERSION, + 'anthropic-beta': providerOptions.betaVersion || PROVIDER_CONSTANTS.ANTHROPIC.BETA_VERSION }, - body: JSON.stringify({ - model, - messages: formattedMessages.messages, - system: formattedMessages.system, - temperature, - max_tokens: opts.maxTokens || 4000, - }) + body: JSON.stringify(requestBody) }); if (!response.ok) { diff --git a/src/services/llm/providers/ollama_service.ts b/src/services/llm/providers/ollama_service.ts index 7b2f04b80..62828f32d 100644 --- a/src/services/llm/providers/ollama_service.ts +++ b/src/services/llm/providers/ollama_service.ts @@ -6,6 +6,8 @@ import { OllamaMessageFormatter } from '../formatters/ollama_formatter.js'; import log from '../../log.js'; import type { ToolCall } from '../tools/tool_interfaces.js'; import toolRegistry from '../tools/tool_registry.js'; +import type { OllamaOptions } from './provider_options.js'; +import { getOllamaOptions } from './providers.js'; interface OllamaFunctionArguments { [key: string]: any; @@ -65,32 +67,33 @@ export class OllamaService extends BaseAIService { throw new Error('Ollama service is not available. Check API URL in settings.'); } - const apiBase = options.getOption('ollamaBaseUrl'); + // Get provider-specific options from the central provider manager + const providerOptions = await getOllamaOptions(opts); - // Get the model name and strip the "ollama:" prefix if it exists - let model = opts.model || options.getOption('ollamaDefaultModel') || 'llama3'; - if (model.startsWith('ollama:')) { - model = model.substring(7); // Remove the "ollama:" prefix - log.info(`Stripped 'ollama:' prefix from model name, using: ${model}`); + // Log provider metadata if available + if (providerOptions.providerMetadata) { + log.info(`Using model ${providerOptions.model} from provider ${providerOptions.providerMetadata.provider}`); + + // Log capabilities if available + const capabilities = providerOptions.providerMetadata.capabilities; + if (capabilities) { + log.info(`Model capabilities: ${JSON.stringify(capabilities)}`); + } } - const temperature = opts.temperature !== undefined - ? opts.temperature - : parseFloat(options.getOption('aiTemperature') || '0.7'); - - const systemPrompt = this.getSystemPrompt(opts.systemPrompt || options.getOption('aiSystemPrompt')); + const systemPrompt = this.getSystemPrompt(providerOptions.systemPrompt || options.getOption('aiSystemPrompt')); try { // Check if we should add tool execution feedback - if (opts.toolExecutionStatus && Array.isArray(opts.toolExecutionStatus) && opts.toolExecutionStatus.length > 0) { + if (providerOptions.toolExecutionStatus && Array.isArray(providerOptions.toolExecutionStatus) && providerOptions.toolExecutionStatus.length > 0) { log.info(`Adding tool execution feedback to messages`); - messages = this.addToolExecutionFeedback(messages, opts.toolExecutionStatus); + messages = this.addToolExecutionFeedback(messages, providerOptions.toolExecutionStatus); } // Determine whether to use the formatter or send messages directly let messagesToSend: Message[]; - if (opts.bypassFormatter) { + if (providerOptions.bypassFormatter) { // Bypass the formatter entirely - use messages as is messagesToSend = [...messages]; log.info(`Bypassing formatter for Ollama request with ${messages.length} messages`); @@ -100,69 +103,58 @@ export class OllamaService extends BaseAIService { messages, systemPrompt, undefined, // context - opts.preserveSystemPrompt + providerOptions.preserveSystemPrompt ); log.info(`Sending to Ollama with formatted messages: ${messagesToSend.length}`); } - // Check if this is a request that expects JSON response - const expectsJsonResponse = opts.expectsJsonResponse || false; - - // Build request body + // Build request body base const requestBody: any = { - model, - messages: messagesToSend, - options: { - temperature, - // Add num_ctx parameter based on model capabilities - num_ctx: await this.getModelContextWindowTokens(model), - // Add response_format for requests that expect JSON - ...(expectsJsonResponse ? { response_format: { type: "json_object" } } : {}) - }, - stream: false + model: providerOptions.model, + messages: messagesToSend }; - // Add tools if enabled - put them at the top level for Ollama - if (opts.enableTools !== false) { - // Get tools from registry if not provided in options - if (!opts.tools || opts.tools.length === 0) { - try { - // Get tool definitions from registry - const tools = toolRegistry.getAllToolDefinitions(); - requestBody.tools = tools; - log.info(`Adding ${tools.length} tools to request`); - // If no tools found, reinitialize - if (tools.length === 0) { - log.info('No tools found in registry, re-initializing...'); - try { - const toolInitializer = await import('../tools/tool_initializer.js'); - await toolInitializer.default.initializeTools(); - - // Try again - requestBody.tools = toolRegistry.getAllToolDefinitions(); - log.info(`After re-initialization: ${requestBody.tools.length} tools available`); - } catch (err: any) { - log.error(`Failed to re-initialize tools: ${err.message}`); - } - } - } catch (error: any) { - log.error(`Error getting tools: ${error.message || String(error)}`); - // Create default empty tools array if we couldn't load the tools - requestBody.tools = []; - } - } else { - requestBody.tools = opts.tools; - } - log.info(`Adding ${requestBody.tools.length} tools to Ollama request`); - } else { - log.info('Tools are explicitly disabled for this request'); + log.info(`Stream: ${providerOptions.stream}`); + // Stream is a top-level option + if (providerOptions.stream !== undefined) { + requestBody.stream = providerOptions.stream; } - // Log key request details + // Add options object if provided + if (providerOptions.options) { + requestBody.options = { ...providerOptions.options }; + } + + // Add tools if enabled + if (providerOptions.enableTools !== false) { + // Use provided tools or get from registry + try { + requestBody.tools = providerOptions.tools && providerOptions.tools.length > 0 + ? providerOptions.tools + : toolRegistry.getAllToolDefinitions(); + + // Handle empty tools array + if (requestBody.tools.length === 0) { + log.info('No tools found, attempting to initialize tools...'); + const toolInitializer = await import('../tools/tool_initializer.js'); + await toolInitializer.default.initializeTools(); + requestBody.tools = toolRegistry.getAllToolDefinitions(); + log.info(`After initialization: ${requestBody.tools.length} tools available`); + } + } catch (error: any) { + log.error(`Error preparing tools: ${error.message || String(error)}`); + requestBody.tools = []; // Empty fallback + } + } + + // Log request details log.info(`========== OLLAMA API REQUEST ==========`); log.info(`Model: ${requestBody.model}, Messages: ${requestBody.messages.length}, Tools: ${requestBody.tools ? requestBody.tools.length : 0}`); - log.info(`Temperature: ${temperature}, Stream: ${requestBody.stream}, JSON response expected: ${expectsJsonResponse}`); + log.info(`Stream: ${requestBody.stream || false}, JSON response expected: ${providerOptions.expectsJsonResponse}`); + if (requestBody.options) { + log.info(`Options: ${JSON.stringify(requestBody.options)}`); + } // Check message structure and log detailed information about each message requestBody.messages.forEach((msg: any, index: number) => { @@ -222,26 +214,13 @@ export class OllamaService extends BaseAIService { log.info(`========== FULL OLLAMA REQUEST ==========`); // Log request in manageable chunks - const maxChunkSize = 4000; - if (requestStr.length > maxChunkSize) { - let i = 0; - while (i < requestStr.length) { - const chunk = requestStr.substring(i, i + maxChunkSize); - log.info(`Request part ${Math.floor(i/maxChunkSize) + 1}/${Math.ceil(requestStr.length/maxChunkSize)}: ${chunk}`); - i += maxChunkSize; - } - } else { - log.info(`Full request: ${requestStr}`); - } + log.info(`Full request: ${requestStr}`); log.info(`========== END FULL OLLAMA REQUEST ==========`); - log.info(`========== END OLLAMA REQUEST ==========`); - // Make API request - const response = await fetch(`${apiBase}/api/chat`, { + // Send the request + const response = await fetch(`${providerOptions.baseUrl}/api/chat`, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); diff --git a/src/services/llm/providers/openai_service.ts b/src/services/llm/providers/openai_service.ts index 98073e6e7..35e159a2f 100644 --- a/src/services/llm/providers/openai_service.ts +++ b/src/services/llm/providers/openai_service.ts @@ -2,6 +2,8 @@ import options from '../../options.js'; import { BaseAIService } from '../base_ai_service.js'; import type { ChatCompletionOptions, ChatResponse, Message } from '../ai_interface.js'; import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; +import type { OpenAIOptions } from './provider_options.js'; +import { getOpenAIOptions } from './providers.js'; export class OpenAIService extends BaseAIService { constructor() { @@ -17,14 +19,10 @@ export class OpenAIService extends BaseAIService { throw new Error('OpenAI service is not available. Check API key and AI settings.'); } - const apiKey = options.getOption('openaiApiKey'); - const baseUrl = options.getOption('openaiBaseUrl') || PROVIDER_CONSTANTS.OPENAI.BASE_URL; - const model = opts.model || options.getOption('openaiDefaultModel') || PROVIDER_CONSTANTS.OPENAI.DEFAULT_MODEL; - const temperature = opts.temperature !== undefined - ? opts.temperature - : parseFloat(options.getOption('aiTemperature') || '0.7'); + // Get provider-specific options from the central provider manager + const providerOptions = getOpenAIOptions(opts); - const systemPrompt = this.getSystemPrompt(opts.systemPrompt || options.getOption('aiSystemPrompt')); + const systemPrompt = this.getSystemPrompt(providerOptions.systemPrompt || options.getOption('aiSystemPrompt')); // Ensure we have a system message const systemMessageExists = messages.some(m => m.role === 'system'); @@ -34,23 +32,52 @@ export class OpenAIService extends BaseAIService { try { // Fix endpoint construction - ensure we don't double up on /v1 - const normalizedBaseUrl = baseUrl.replace(/\/+$/, ''); + const normalizedBaseUrl = providerOptions.baseUrl.replace(/\/+$/, ''); const endpoint = normalizedBaseUrl.includes('/v1') ? `${normalizedBaseUrl}/chat/completions` : `${normalizedBaseUrl}/v1/chat/completions`; + // Create request body directly from provider options + const requestBody: any = { + model: providerOptions.model, + messages: messagesWithSystem, + }; + + // Extract API parameters from provider options + const apiParams = { + temperature: providerOptions.temperature, + max_tokens: providerOptions.max_tokens, + stream: providerOptions.stream, + top_p: providerOptions.top_p, + frequency_penalty: providerOptions.frequency_penalty, + presence_penalty: providerOptions.presence_penalty + }; + + + + // Merge API parameters, filtering out undefined values + Object.entries(apiParams).forEach(([key, value]) => { + if (value !== undefined) { + requestBody[key] = value; + } + }); + + // Add tools if enabled + if (providerOptions.enableTools && providerOptions.tools && providerOptions.tools.length > 0) { + requestBody.tools = providerOptions.tools; + } + + if (providerOptions.tool_choice) { + requestBody.tool_choice = providerOptions.tool_choice; + } + const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` + 'Authorization': `Bearer ${providerOptions.apiKey}` }, - body: JSON.stringify({ - model, - messages: messagesWithSystem, - temperature, - max_tokens: opts.maxTokens, - }) + body: JSON.stringify(requestBody) }); if (!response.ok) { @@ -68,7 +95,8 @@ export class OpenAIService extends BaseAIService { promptTokens: data.usage?.prompt_tokens, completionTokens: data.usage?.completion_tokens, totalTokens: data.usage?.total_tokens - } + }, + tool_calls: data.choices[0].message.tool_calls }; } catch (error) { console.error('OpenAI service error:', error); diff --git a/src/services/llm/providers/provider_options.ts b/src/services/llm/providers/provider_options.ts new file mode 100644 index 000000000..c765c2738 --- /dev/null +++ b/src/services/llm/providers/provider_options.ts @@ -0,0 +1,202 @@ +import type { Message, ChatCompletionOptions } from '../ai_interface.js'; +import type { ToolCall } from '../tools/tool_interfaces.js'; + +/** + * Model metadata interface to track provider information + */ +export interface ModelMetadata { + // The provider that supports this model + provider: 'openai' | 'anthropic' | 'ollama' | 'local'; + // The actual model identifier used by the provider's API + modelId: string; + // Display name for UI (optional) + displayName?: string; + // Model capabilities + capabilities?: { + contextWindow?: number; + supportsTools?: boolean; + supportsVision?: boolean; + supportsStreaming?: boolean; + }; +} + +/** + * Base provider configuration that's common to all providers + * but not necessarily sent directly to APIs + */ +export interface ProviderConfig { + // Internal configuration + systemPrompt?: string; + // Provider metadata for model routing + providerMetadata?: ModelMetadata; +} + +/** + * OpenAI-specific options, structured to match the OpenAI API + */ +export interface OpenAIOptions extends ProviderConfig { + // Connection settings (not sent to API) + apiKey: string; + baseUrl: string; + + // Direct API parameters as they appear in requests + model: string; + messages?: Message[]; + temperature?: number; + max_tokens?: number; + stream?: boolean; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; + tools?: any[]; + tool_choice?: string | object; + + // Internal control flags (not sent directly to API) + enableTools?: boolean; +} + +/** + * Anthropic-specific options, structured to match the Anthropic API + */ +export interface AnthropicOptions extends ProviderConfig { + // Connection settings (not sent to API) + apiKey: string; + baseUrl: string; + apiVersion?: string; + betaVersion?: string; + + // Direct API parameters as they appear in requests + model: string; + messages?: any[]; + system?: string; + temperature?: number; + max_tokens?: number; + stream?: boolean; + top_p?: number; + + // Internal parameters (not sent directly to API) + formattedMessages?: { messages: any[], system: string }; +} + +/** + * Ollama-specific options, structured to match the Ollama API + */ +export interface OllamaOptions extends ProviderConfig { + // Connection settings (not sent to API) + baseUrl: string; + + // Direct API parameters as they appear in requests + model: string; + messages?: Message[]; + stream?: boolean; + options?: { + temperature?: number; + num_ctx?: number; + top_p?: number; + top_k?: number; + num_predict?: number; // equivalent to max_tokens + response_format?: { type: string }; + }; + tools?: any[]; + + // Internal control flags (not sent directly to API) + enableTools?: boolean; + bypassFormatter?: boolean; + preserveSystemPrompt?: boolean; + expectsJsonResponse?: boolean; + toolExecutionStatus?: any[]; +} + +/** + * Create OpenAI options from generic options and config + */ +export function createOpenAIOptions( + opts: ChatCompletionOptions = {}, + apiKey: string, + baseUrl: string, + defaultModel: string +): OpenAIOptions { + return { + // Connection settings + apiKey, + baseUrl, + + // API parameters + model: opts.model || defaultModel, + temperature: opts.temperature, + max_tokens: opts.maxTokens, + stream: opts.stream, + top_p: opts.topP, + frequency_penalty: opts.frequencyPenalty, + presence_penalty: opts.presencePenalty, + tools: opts.tools, + + // Internal configuration + systemPrompt: opts.systemPrompt, + enableTools: opts.enableTools, + }; +} + +/** + * Create Anthropic options from generic options and config + */ +export function createAnthropicOptions( + opts: ChatCompletionOptions = {}, + apiKey: string, + baseUrl: string, + defaultModel: string, + apiVersion: string, + betaVersion: string +): AnthropicOptions { + return { + // Connection settings + apiKey, + baseUrl, + apiVersion, + betaVersion, + + // API parameters + model: opts.model || defaultModel, + temperature: opts.temperature, + max_tokens: opts.maxTokens, + stream: opts.stream, + top_p: opts.topP, + + // Internal configuration + systemPrompt: opts.systemPrompt, + }; +} + +/** + * Create Ollama options from generic options and config + */ +export function createOllamaOptions( + opts: ChatCompletionOptions = {}, + baseUrl: string, + defaultModel: string, + contextWindow: number +): OllamaOptions { + return { + // Connection settings + baseUrl, + + // API parameters + model: opts.model || defaultModel, + stream: opts.stream, + options: { + temperature: opts.temperature, + num_ctx: contextWindow, + num_predict: opts.maxTokens, + response_format: opts.expectsJsonResponse ? { type: "json_object" } : undefined + }, + tools: opts.tools, + + // Internal configuration + systemPrompt: opts.systemPrompt, + enableTools: opts.enableTools, + bypassFormatter: opts.bypassFormatter, + preserveSystemPrompt: opts.preserveSystemPrompt, + expectsJsonResponse: opts.expectsJsonResponse, + toolExecutionStatus: opts.toolExecutionStatus, + }; +} diff --git a/src/services/llm/providers/providers.ts b/src/services/llm/providers/providers.ts index 73ca01e44..91fb0f172 100644 --- a/src/services/llm/providers/providers.ts +++ b/src/services/llm/providers/providers.ts @@ -9,6 +9,14 @@ import { OpenAIEmbeddingProvider } from "../embeddings/providers/openai.js"; import { OllamaEmbeddingProvider } from "../embeddings/providers/ollama.js"; import { VoyageEmbeddingProvider } from "../embeddings/providers/voyage.js"; import type { OptionDefinitions } from "../../options_interface.js"; +import type { ChatCompletionOptions } from '../ai_interface.js'; +import type { OpenAIOptions, AnthropicOptions, OllamaOptions, ModelMetadata } from './provider_options.js'; +import { + createOpenAIOptions, + createAnthropicOptions, + createOllamaOptions +} from './provider_options.js'; +import { PROVIDER_CONSTANTS } from '../constants/provider_constants.js'; /** * Simple local embedding provider implementation @@ -362,3 +370,238 @@ export default { getEmbeddingProviderConfigs, initializeDefaultProviders }; + +/** + * Get OpenAI provider options from chat options and configuration + * Updated to use provider metadata approach + */ +export function getOpenAIOptions( + opts: ChatCompletionOptions = {} +): OpenAIOptions { + try { + const apiKey = options.getOption('openaiApiKey'); + if (!apiKey) { + throw new Error('OpenAI API key is not configured'); + } + + const baseUrl = options.getOption('openaiBaseUrl') || PROVIDER_CONSTANTS.OPENAI.BASE_URL; + const modelName = opts.model || options.getOption('openaiDefaultModel') || PROVIDER_CONSTANTS.OPENAI.DEFAULT_MODEL; + + // Create provider metadata + const providerMetadata: ModelMetadata = { + provider: 'openai', + modelId: modelName, + displayName: modelName, + capabilities: { + supportsTools: modelName.includes('gpt-4') || modelName.includes('gpt-3.5-turbo'), + supportsVision: modelName.includes('vision') || modelName.includes('gpt-4-turbo') || modelName.includes('gpt-4o'), + supportsStreaming: true + } + }; + + // Get temperature from options or global setting + const temperature = opts.temperature !== undefined + ? opts.temperature + : parseFloat(options.getOption('aiTemperature') || '0.7'); + + return { + // Connection settings + apiKey, + baseUrl, + + // Provider metadata + providerMetadata, + + // API parameters + model: modelName, + temperature, + max_tokens: opts.maxTokens, + stream: opts.stream, + top_p: opts.topP, + frequency_penalty: opts.frequencyPenalty, + presence_penalty: opts.presencePenalty, + tools: opts.tools, + + // Internal configuration + systemPrompt: opts.systemPrompt, + enableTools: opts.enableTools, + }; + } catch (error) { + log.error(`Error creating OpenAI provider options: ${error}`); + throw error; + } +} + +/** + * Get Anthropic provider options from chat options and configuration + * Updated to use provider metadata approach + */ +export function getAnthropicOptions( + opts: ChatCompletionOptions = {} +): AnthropicOptions { + try { + const apiKey = options.getOption('anthropicApiKey'); + if (!apiKey) { + throw new Error('Anthropic API key is not configured'); + } + + const baseUrl = options.getOption('anthropicBaseUrl') || PROVIDER_CONSTANTS.ANTHROPIC.BASE_URL; + const modelName = opts.model || options.getOption('anthropicDefaultModel') || PROVIDER_CONSTANTS.ANTHROPIC.DEFAULT_MODEL; + + // Create provider metadata + const providerMetadata: ModelMetadata = { + provider: 'anthropic', + modelId: modelName, + displayName: modelName, + capabilities: { + supportsTools: modelName.includes('claude-3') || modelName.includes('claude-3.5'), + supportsVision: modelName.includes('claude-3') || modelName.includes('claude-3.5'), + supportsStreaming: true, + // Anthropic models typically have large context windows + contextWindow: modelName.includes('claude-3-opus') ? 200000 : + modelName.includes('claude-3-sonnet') ? 180000 : + modelName.includes('claude-3.5-sonnet') ? 200000 : 100000 + } + }; + + // Get temperature from options or global setting + const temperature = opts.temperature !== undefined + ? opts.temperature + : parseFloat(options.getOption('aiTemperature') || '0.7'); + + return { + // Connection settings + apiKey, + baseUrl, + apiVersion: PROVIDER_CONSTANTS.ANTHROPIC.API_VERSION, + betaVersion: PROVIDER_CONSTANTS.ANTHROPIC.BETA_VERSION, + + // Provider metadata + providerMetadata, + + // API parameters + model: modelName, + temperature, + max_tokens: opts.maxTokens, + stream: opts.stream, + top_p: opts.topP, + + // Internal configuration + systemPrompt: opts.systemPrompt + }; + } catch (error) { + log.error(`Error creating Anthropic provider options: ${error}`); + throw error; + } +} + +/** + * Get Ollama provider options from chat options and configuration + * This implementation cleanly separates provider information from model names + */ +export async function getOllamaOptions( + opts: ChatCompletionOptions = {}, + contextWindow?: number +): Promise { + try { + const baseUrl = options.getOption('ollamaBaseUrl'); + if (!baseUrl) { + throw new Error('Ollama API URL is not configured'); + } + + // Get the model name - no prefix handling needed now + let modelName = opts.model || options.getOption('ollamaDefaultModel') || 'llama3'; + + // Create provider metadata + const providerMetadata: ModelMetadata = { + provider: 'ollama', + modelId: modelName, + capabilities: { + supportsTools: true, + supportsStreaming: true + } + }; + + // Get temperature from options or global setting + const temperature = opts.temperature !== undefined + ? opts.temperature + : parseFloat(options.getOption('aiTemperature') || '0.7'); + + // Use provided context window or get from model if not specified + const modelContextWindow = contextWindow || await getOllamaModelContextWindow(modelName); + + // Update capabilities with context window information + providerMetadata.capabilities!.contextWindow = modelContextWindow; + + return { + // Connection settings + baseUrl, + + // Provider metadata + providerMetadata, + + // API parameters + model: modelName, // Clean model name without provider prefix + stream: opts.stream, + options: { + temperature: opts.temperature, + num_ctx: modelContextWindow, + num_predict: opts.maxTokens, + response_format: opts.expectsJsonResponse ? { type: "json_object" } : undefined + }, + tools: opts.tools, + + // Internal configuration + systemPrompt: opts.systemPrompt, + enableTools: opts.enableTools, + bypassFormatter: opts.bypassFormatter, + preserveSystemPrompt: opts.preserveSystemPrompt, + expectsJsonResponse: opts.expectsJsonResponse, + toolExecutionStatus: opts.toolExecutionStatus, + }; + } catch (error) { + log.error(`Error creating Ollama provider options: ${error}`); + throw error; + } +} + +/** + * Get context window size for Ollama model + */ +async function getOllamaModelContextWindow(modelName: string): Promise { + try { + const baseUrl = options.getOption('ollamaBaseUrl'); + + // Try to get model information from Ollama API + const response = await fetch(`${baseUrl}/api/show`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: modelName }) + }); + + if (response.ok) { + const data = await response.json(); + // Get context window from model parameters + if (data && data.parameters && data.parameters.num_ctx) { + return data.parameters.num_ctx; + } + } + + // Default context sizes by model family if we couldn't get specific info + if (modelName.includes('llama3')) { + return 8192; + } else if (modelName.includes('llama2')) { + return 4096; + } else if (modelName.includes('mistral') || modelName.includes('mixtral')) { + return 8192; + } else if (modelName.includes('gemma')) { + return 8192; + } + + // Return a reasonable default + return 4096; + } catch (error) { + log.info(`Error getting context window for model ${modelName}: ${error}`); + return 4096; // Default fallback + } +}