immersive2/server/services/providerConfig.js
Michael Mainguy 03217f3e65 Add Cloudflare Workers AI provider and multiple AI chat improvements
- Add Cloudflare Workers AI as third provider alongside Claude and Ollama
  - New cloudflare.js API handler with format conversion
  - Tool converter functions for Cloudflare's OpenAI-compatible format
  - Handle [TOOL_CALLS] and [Called tool:] text formats from Mistral
  - Robust parser that handles truncated JSON responses

- Add usage tracking with cost display
  - New usageTracker.js service for tracking token usage per session
  - UsageDetailModal component showing per-request breakdown
  - Cost display in ChatPanel header

- Add new diagram manipulation features
  - Entity scale and rotation support via modify_entity tool
  - Wikipedia search tool for researching topics before diagramming
  - Clear conversation tool to reset chat history
  - JSON import from hamburger menu (moved from ChatPanel)

- Fix connection label rotation in billboard mode
  - Labels no longer have conflicting local rotation when billboard enabled
  - Update rotation when rendering mode changes

- Improve tool calling reliability
  - Add MAX_TOOL_ITERATIONS safety limit
  - Break loop after model switch to prevent context issues
  - Increase max_tokens to 4096 to prevent truncation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 06:31:43 -06:00

141 lines
3.7 KiB
JavaScript

/**
* AI Provider Configuration
* Manages configuration for different AI providers (Claude, Ollama, Cloudflare)
*/
// Default configuration
const DEFAULT_PROVIDER = 'claude';
// Use 127.0.0.1 instead of localhost to avoid IPv6 resolution issues
const DEFAULT_OLLAMA_URL = 'http://127.0.0.1:11434';
const DEFAULT_CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID || '';
/**
* Get the current AI provider
* @returns {string} Provider name ('claude' or 'ollama')
*/
export function getProvider() {
return process.env.AI_PROVIDER || DEFAULT_PROVIDER;
}
/**
* Get Ollama API URL
* @returns {string} Ollama base URL
*/
export function getOllamaUrl() {
return process.env.OLLAMA_URL || DEFAULT_OLLAMA_URL;
}
/**
* Get Anthropic API URL
* @returns {string} Anthropic base URL
*/
export function getAnthropicUrl() {
return process.env.ANTHROPIC_API_URL || 'https://api.anthropic.com';
}
/**
* Get Cloudflare Account ID
* @returns {string} Cloudflare account ID
*/
export function getCloudflareAccountId() {
return process.env.CLOUDFLARE_ACCOUNT_ID || DEFAULT_CLOUDFLARE_ACCOUNT_ID;
}
/**
* Get Cloudflare API Token
* @returns {string} Cloudflare API token
*/
export function getCloudflareApiToken() {
return process.env.CLOUDFLARE_API_TOKEN || '';
}
/**
* Get Cloudflare Workers AI base URL
* @returns {string} Cloudflare Workers AI base URL
*/
export function getCloudflareUrl() {
const accountId = getCloudflareAccountId();
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run`;
}
/**
* Get provider configuration for a specific provider
* @param {string} provider - Provider name
* @returns {object} Provider configuration
*/
export function getProviderConfig(provider) {
switch (provider) {
case 'ollama':
return {
name: 'ollama',
baseUrl: getOllamaUrl(),
chatEndpoint: '/api/chat',
requiresAuth: false
};
case 'cloudflare':
return {
name: 'cloudflare',
baseUrl: getCloudflareUrl(),
chatEndpoint: '', // Model is appended to baseUrl
requiresAuth: true,
apiKey: getCloudflareApiToken(),
accountId: getCloudflareAccountId()
};
case 'claude':
default:
return {
name: 'claude',
baseUrl: getAnthropicUrl(),
chatEndpoint: '/v1/messages',
requiresAuth: true,
apiKey: process.env.ANTHROPIC_API_KEY
};
}
}
/**
* Determine provider from model ID
* @param {string} modelId - Model identifier
* @returns {string} Provider name
*/
export function getProviderFromModel(modelId) {
if (!modelId) return getProvider();
// Claude models start with 'claude-'
if (modelId.startsWith('claude-')) {
return 'claude';
}
// Cloudflare models start with '@cf/' or '@hf/'
if (modelId.startsWith('@cf/') || modelId.startsWith('@hf/')) {
return 'cloudflare';
}
// Known Ollama models
const ollamaModels = [
'llama', 'mistral', 'qwen', 'codellama', 'phi',
'gemma', 'neural-chat', 'starling', 'orca', 'vicuna',
'deepseek', 'dolphin', 'nous-hermes', 'openhermes'
];
for (const prefix of ollamaModels) {
if (modelId.toLowerCase().startsWith(prefix)) {
return 'ollama';
}
}
// Default to configured provider
return getProvider();
}
export default {
getProvider,
getOllamaUrl,
getAnthropicUrl,
getCloudflareAccountId,
getCloudflareApiToken,
getCloudflareUrl,
getProviderConfig,
getProviderFromModel
};