Merge pull request #5 from bayer-int/feature/bayer-mga-provider

Feature/bayer mga provider
This commit is contained in:
Nirmal Arya 2025-05-31 14:37:32 -04:00 committed by GitHub
commit 198e6c0ec1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 33922 additions and 7 deletions

View File

@ -19,6 +19,7 @@ import type { IconType } from 'react-icons';
type ProviderName =
| 'AmazonBedrock'
| 'Anthropic'
| 'BayerMGA'
| 'Cohere'
| 'Deepseek'
| 'Google'
@ -36,6 +37,7 @@ type ProviderName =
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
AmazonBedrock: SiAmazon,
Anthropic: FaBrain,
BayerMGA: BsRobot,
Cohere: BiChip,
Deepseek: BiCodeBlock,
Google: SiGoogle,
@ -53,6 +55,7 @@ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
// Update PROVIDER_DESCRIPTIONS to use the same type
const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
Anthropic: 'Access Claude and other Anthropic models',
BayerMGA: 'Connect to Bayer\'s internal AI assistant platform',
OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
};

View File

@ -16,11 +16,13 @@ import { useToast } from '~/components/ui/use-toast';
type ProviderName =
| 'AmazonBedrock'
| 'Anthropic'
| 'BayerMGA'
| 'Cohere'
| 'Deepseek'
| 'Google'
| 'Groq'
| 'HuggingFace'
| 'Hyperbolic'
| 'Mistral'
| 'OpenAI'
| 'OpenRouter'
@ -82,6 +84,14 @@ const PROVIDER_STATUS_URLS: Record<ProviderName, ProviderConfig> = {
},
testModel: 'claude-3-sonnet-20240229',
},
BayerMGA: {
statusUrl: 'https://chat.int.bayer.com',
apiUrl: 'https://chat.int.bayer.com/api/v2/models?include_hidden_models=false&include_aliases=true',
headers: {
Authorization: 'Bearer $BAYER_MGA_API_KEY',
},
testModel: 'gpt-4o',
},
Cohere: {
statusUrl: 'https://status.cohere.com/',
apiUrl: 'https://api.cohere.ai/v1/models',
@ -170,11 +180,20 @@ const PROVIDER_STATUS_URLS: Record<ProviderName, ProviderConfig> = {
},
testModel: 'deepseek-chat',
},
Hyperbolic: {
statusUrl: 'https://status.hyperbolic.ai/',
apiUrl: 'https://api.hyperbolic.ai/v1/models',
headers: {
Authorization: 'Bearer $HYPERBOLIC_API_KEY',
},
testModel: 'claude-3-opus-20240229',
},
};
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
AmazonBedrock: SiAmazon,
Anthropic: FaBrain,
BayerMGA: BsRobot,
Cohere: BiChip,
Google: SiGoogle,
Groq: BsCloud,
@ -186,6 +205,7 @@ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
Together: BsCloud,
XAI: BsRobot,
Deepseek: BiCodeBlock,
Hyperbolic: BsCloud,
};
const ServiceStatusTab = () => {
@ -209,6 +229,7 @@ const ServiceStatusTab = () => {
const envKeyMap: Record<ProviderName, string> = {
OpenAI: 'OPENAI_API_KEY',
Anthropic: 'ANTHROPIC_API_KEY',
BayerMGA: 'BAYER_MGA_API_KEY',
Cohere: 'COHERE_API_KEY',
Google: 'GOOGLE_GENERATIVE_AI_API_KEY',
HuggingFace: 'HuggingFace_API_KEY',
@ -220,6 +241,7 @@ const ServiceStatusTab = () => {
OpenRouter: 'OPEN_ROUTER_API_KEY',
XAI: 'XAI_API_KEY',
Deepseek: 'DEEPSEEK_API_KEY',
Hyperbolic: 'HYPERBOLIC_API_KEY',
};
const envKey = envKeyMap[provider];

View File

@ -0,0 +1,140 @@
import { BaseProvider } from '~/lib/modules/llm/base-provider';
import type { ModelInfo } from '~/lib/modules/llm/types';
import type { IProviderSetting } from '~/types/model';
import type { LanguageModelV1 } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { createScopedLogger } from '~/utils/logger';
const logger = createScopedLogger('BayerMGAProvider');
export default class BayerMGAProvider extends BaseProvider {
name = 'BayerMGA';
getApiKeyLink = 'https://chat.int.bayer.com';
labelForGetApiKey = 'Get Bayer MGA API Key';
config = {
baseUrl: 'https://chat.int.bayer.com/api/v2',
apiTokenKey: 'BAYER_MGA_API_KEY',
};
staticModels: ModelInfo[] = [
// Temporary static models for testing inference flow
{ name: 'claude-3-7-sonnet', label: 'Claude 3.7 Sonnet', provider: 'BayerMGA', maxTokenAllowed: 128000 },
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'BayerMGA', maxTokenAllowed: 128000 },
// Add other common models if they appear in the UI for testing purposes
];
async getDynamicModels(
apiKeys?: Record<string, string>,
settings?: IProviderSetting,
serverEnv: Record<string, string> = {},
): Promise<ModelInfo[]> {
try {
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: settings,
serverEnv,
defaultBaseUrlKey: 'BAYER_MGA_API_BASE_URL',
defaultApiTokenKey: 'BAYER_MGA_API_KEY',
});
if (!baseUrl || !apiKey) {
logger.warn('Missing baseUrl or apiKey configuration for Bayer MGA provider');
return [];
}
// Normalize base URL (remove trailing slash)
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
// Construct models URL with query parameters
const modelsUrl = `${normalizedBaseUrl}/models?include_hidden_models=false&include_aliases=true`;
logger.info(`Fetching models from ${modelsUrl}`);
// Make API request with proper headers
const response = await fetch(modelsUrl, {
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
});
// Handle HTTP errors
if (!response.ok) {
const errorText = await response.text();
logger.error(`Failed to fetch Bayer MGA models: ${response.status} ${response.statusText}`);
logger.error(`Error details: ${errorText}`);
return [];
}
// Parse response
const responseText = await response.text();
let res;
try {
res = JSON.parse(responseText);
} catch (e) {
logger.error(`Invalid JSON response from Bayer MGA API: ${responseText.substring(0, 200)}...`);
return [];
}
// Validate response structure
if (!res.data || !Array.isArray(res.data)) {
logger.error(`Invalid response format from Bayer MGA API: missing data array`);
logger.debug(`Response: ${JSON.stringify(res).substring(0, 500)}...`);
return [];
}
// Filter for available models and map to ModelInfo format
const models = res.data
.filter((model: any) => model.model_status === 'available')
.map((model: any) => ({
name: model.model,
label: model.name || model.model,
provider: this.name,
maxTokenAllowed: model.context_window || 8000,
}));
logger.info(`Found ${models.length} available models from Bayer MGA`);
return models;
} catch (error) {
logger.error(`Error fetching Bayer MGA models: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}
getModelInstance(options: {
model: string;
serverEnv: Env;
apiKeys?: Record<string, string>;
providerSettings?: Record<string, IProviderSetting>;
}): LanguageModelV1 {
const { model, serverEnv, apiKeys, providerSettings } = options;
const { baseUrl, apiKey } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
serverEnv: serverEnv as any,
defaultBaseUrlKey: 'BAYER_MGA_API_BASE_URL',
defaultApiTokenKey: 'BAYER_MGA_API_KEY',
});
if (!apiKey) {
throw new Error(`Missing API key for ${this.name} provider`);
}
if (!baseUrl) {
throw new Error(`Missing base URL for ${this.name} provider`);
}
// Normalize base URL (remove trailing slash)
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
logger.info(`Creating model instance for ${model} using Bayer MGA API at ${normalizedBaseUrl}`);
const openai = createOpenAI({
baseURL: normalizedBaseUrl,
apiKey,
});
return openai(model);
}
}

View File

@ -1,9 +1,13 @@
import AmazonBedrockProvider from './providers/amazon-bedrock';
import AnthropicProvider from './providers/anthropic';
import BayerMGAProvider from './providers/bayer-mga';
import CohereProvider from './providers/cohere';
import DeepseekProvider from './providers/deepseek';
import GithubProvider from './providers/github';
import GoogleProvider from './providers/google';
import GroqProvider from './providers/groq';
import HuggingFaceProvider from './providers/huggingface';
import HyperbolicProvider from './providers/hyperbolic';
import LMStudioProvider from './providers/lmstudio';
import MistralProvider from './providers/mistral';
import OllamaProvider from './providers/ollama';
@ -13,27 +17,25 @@ import OpenAIProvider from './providers/openai';
import PerplexityProvider from './providers/perplexity';
import TogetherProvider from './providers/together';
import XAIProvider from './providers/xai';
import HyperbolicProvider from './providers/hyperbolic';
import AmazonBedrockProvider from './providers/amazon-bedrock';
import GithubProvider from './providers/github';
export {
AmazonBedrockProvider,
AnthropicProvider,
BayerMGAProvider,
CohereProvider,
DeepseekProvider,
GithubProvider,
GoogleProvider,
GroqProvider,
HuggingFaceProvider,
HyperbolicProvider,
LMStudioProvider,
MistralProvider,
OllamaProvider,
OpenAIProvider,
OpenRouterProvider,
OpenAILikeProvider,
PerplexityProvider,
XAIProvider,
TogetherProvider,
LMStudioProvider,
AmazonBedrockProvider,
GithubProvider,
XAIProvider,
};

View File

@ -0,0 +1,286 @@
import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from '@remix-run/cloudflare';
import { getApiKeysFromCookie, getProviderSettingsFromCookie } from '~/lib/api/cookies';
import { createScopedLogger } from '~/utils/logger';
import { BaseProvider, getOpenAILikeModel } from '~/lib/modules/llm/base-provider';
import { generateText } from 'ai';
const logger = createScopedLogger('BayerMGADebug');
// Add CORS headers to all responses
function addCorsHeaders(response: Response): Response {
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
}
// Handle OPTIONS requests for CORS preflight
export async function loader({ request }: LoaderFunctionArgs) {
if (request.method === 'OPTIONS') {
return addCorsHeaders(new Response(null, { status: 204 }));
}
const url = new URL(request.url);
const apiKey = url.searchParams.get('apiKey');
const baseUrl = url.searchParams.get('baseUrl') || 'https://chat.int.bayer.com/api/v2';
const model = url.searchParams.get('model') || 'gpt-4o-mini';
// Get API key from cookies if not provided in query params
let effectiveApiKey = apiKey;
if (!effectiveApiKey) {
const cookieHeader = request.headers.get('Cookie');
const apiKeys = getApiKeysFromCookie(cookieHeader);
effectiveApiKey = apiKeys?.['BayerMGA'];
}
if (!effectiveApiKey) {
return addCorsHeaders(
json(
{
success: false,
error: 'No API key provided. Add ?apiKey=your_key to the URL or set it in the app settings.',
tests: [],
},
{ status: 400 },
),
);
}
// Normalize base URL (remove trailing slash)
const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
// Results container
const results = {
success: true,
baseUrl: normalizedBaseUrl,
model,
apiKeyProvided: !!effectiveApiKey,
apiKeyPrefix: effectiveApiKey ? `${effectiveApiKey.substring(0, 4)}...` : null,
tests: [] as any[],
};
// Test 1: Fetch models
try {
logger.info(`Testing models endpoint: ${normalizedBaseUrl}/models`);
const modelsUrl = `${normalizedBaseUrl}/models?include_hidden_models=false&include_aliases=true`;
const modelsStartTime = Date.now();
const modelsResponse = await fetch(modelsUrl, {
headers: {
Authorization: `Bearer ${effectiveApiKey}`,
},
});
const modelsEndTime = Date.now();
const modelsResponseText = await modelsResponse.text();
let modelsData;
let parsedModels = [];
try {
modelsData = JSON.parse(modelsResponseText);
if (modelsData.data && Array.isArray(modelsData.data)) {
parsedModels = modelsData.data
.filter((model: any) => model.model_status === 'available')
.map((model: any) => ({
model: model.model,
name: model.name,
context_window: model.context_window,
}));
}
} catch (e) {
// JSON parse error
}
results.tests.push({
name: 'Models Endpoint',
url: modelsUrl,
success: modelsResponse.ok,
status: modelsResponse.status,
statusText: modelsResponse.statusText,
duration: `${modelsEndTime - modelsStartTime}ms`,
headers: Object.fromEntries(modelsResponse.headers.entries()),
responsePreview: modelsResponseText.substring(0, 500) + (modelsResponseText.length > 500 ? '...' : ''),
parsedModelsCount: parsedModels.length,
availableModels: parsedModels.slice(0, 5), // First 5 models only
});
// If models test failed, don't attempt completions test
if (!modelsResponse.ok) {
results.success = false;
return addCorsHeaders(json(results));
}
// Test 2: Chat completions
logger.info(`Testing chat completions endpoint: ${normalizedBaseUrl}/chat/completions`);
const completionsUrl = `${normalizedBaseUrl}/chat/completions`;
const completionsStartTime = Date.now();
const completionsResponse = await fetch(completionsUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${effectiveApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: model,
messages: [
{
role: 'user',
content: 'Hello from Buildify debug test. Please respond with a very short greeting.',
},
],
temperature: 0.2,
stream: false,
}),
});
const completionsEndTime = Date.now();
const completionsResponseText = await completionsResponse.text();
let completionsData;
let completionContent = null;
try {
completionsData = JSON.parse(completionsResponseText);
if (completionsData.choices && completionsData.choices.length > 0) {
completionContent = completionsData.choices[0].message?.content;
}
} catch (e) {
// JSON parse error
}
results.tests.push({
name: 'Chat Completions Endpoint',
url: completionsUrl,
success: completionsResponse.ok,
status: completionsResponse.status,
statusText: completionsResponse.statusText,
duration: `${completionsEndTime - completionsStartTime}ms`,
headers: Object.fromEntries(completionsResponse.headers.entries()),
responsePreview: completionsResponseText.substring(0, 500) + (completionsResponseText.length > 500 ? '...' : ''),
completionContent,
});
// Test 3: Using the AI SDK (same as the app)
try {
logger.info(`Testing AI SDK integration with ${normalizedBaseUrl}`);
const sdkStartTime = Date.now();
// This mimics exactly how the app uses the provider
const openaiLike = getOpenAILikeModel(normalizedBaseUrl, effectiveApiKey, model);
const result = await generateText({
messages: [
{
role: 'user',
content: 'Hello from Buildify AI SDK test. Please respond with a very short greeting.',
},
],
model: openaiLike,
maxTokens: 100,
});
const sdkEndTime = Date.now();
results.tests.push({
name: 'AI SDK Integration',
success: true,
duration: `${sdkEndTime - sdkStartTime}ms`,
result: result,
});
} catch (error) {
results.success = false;
results.tests.push({
name: 'AI SDK Integration',
success: false,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
}
return addCorsHeaders(json(results));
} catch (error) {
results.success = false;
results.error = error instanceof Error ? error.message : String(error);
results.errorStack = error instanceof Error ? error.stack : undefined;
return addCorsHeaders(json(results, { status: 500 }));
}
}
// Handle POST requests for more complex testing
export async function action({ request, context }: ActionFunctionArgs) {
if (request.method === 'OPTIONS') {
return addCorsHeaders(new Response(null, { status: 204 }));
}
try {
const { apiKey, baseUrl, model, messages, system } = await request.json();
// Get API key from cookies if not provided in request body
let effectiveApiKey = apiKey;
if (!effectiveApiKey) {
const cookieHeader = request.headers.get('Cookie');
const apiKeys = getApiKeysFromCookie(cookieHeader);
effectiveApiKey = apiKeys?.['BayerMGA'];
}
if (!effectiveApiKey) {
return addCorsHeaders(
json(
{
success: false,
error: 'No API key provided in request body or cookies',
},
{ status: 400 },
),
);
}
// Normalize base URL
const normalizedBaseUrl = (baseUrl || 'https://chat.int.bayer.com/api/v2').endsWith('/')
? (baseUrl || 'https://chat.int.bayer.com/api/v2').slice(0, -1)
: (baseUrl || 'https://chat.int.bayer.com/api/v2');
const effectiveModel = model || 'gpt-4o-mini';
logger.info(`Testing AI SDK integration with custom messages: ${normalizedBaseUrl}`);
// Create OpenAI-like model instance (same as the provider)
const openaiLike = getOpenAILikeModel(normalizedBaseUrl, effectiveApiKey, effectiveModel);
// Generate text using the AI SDK
const result = await generateText({
system: system || undefined,
messages: messages || [
{
role: 'user',
content: 'Hello from Buildify custom test. Please respond with a very short greeting.',
},
],
model: openaiLike,
maxTokens: 500,
});
return addCorsHeaders(
json({
success: true,
baseUrl: normalizedBaseUrl,
model: effectiveModel,
result,
}),
);
} catch (error) {
logger.error(`Error in debug action: ${error instanceof Error ? error.message : String(error)}`);
return addCorsHeaders(
json(
{
success: false,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
},
{ status: 500 },
),
);
}
}

33319
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

143
test-bayer-mga.js Normal file
View File

@ -0,0 +1,143 @@
// test-bayer-mga.js
// A Node.js test script to debug the BayerMGA provider and identify the 500 error
// Import required modules
const path = require('path');
const { generateText } = require('ai');
// Set up paths for imports
const APP_ROOT = path.resolve(__dirname);
const PROVIDER_PATH = path.join(APP_ROOT, 'app/lib/modules/llm/providers/bayer-mga.js');
// Import the BayerMGA provider directly
let BayerMGAProvider;
try {
// Try CommonJS import first
BayerMGAProvider = require(PROVIDER_PATH).default;
} catch (error) {
console.error(`Error importing BayerMGA provider: ${error.message}`);
console.error(`Make sure the path is correct: ${PROVIDER_PATH}`);
process.exit(1);
}
// Test API key - replace with a real one for actual testing
const TEST_API_KEY = process.env.BAYER_MGA_API_KEY || 'your-test-api-key';
// Mock parameters similar to what /api/llmcall would use
const mockApiKeys = {
BayerMGA: TEST_API_KEY
};
const mockProviderSettings = {
BayerMGA: {
enabled: true,
baseUrl: 'https://chat.int.bayer.com/api/v2'
}
};
const mockServerEnv = {
BAYER_MGA_API_KEY: TEST_API_KEY,
BAYER_MGA_API_BASE_URL: 'https://chat.int.bayer.com/api/v2'
};
// Test model - use one that appears in the UI
const TEST_MODEL = 'claude-3-7-sonnet';
async function testBayerMGAProvider() {
console.log('='.repeat(80));
console.log('BAYER MGA PROVIDER TEST');
console.log('='.repeat(80));
try {
console.log('Step 1: Creating BayerMGA provider instance...');
const provider = new BayerMGAProvider();
console.log('✅ Provider instance created successfully');
console.log(`Provider name: ${provider.name}`);
console.log(`Provider config:`, JSON.stringify(provider.config, null, 2));
console.log('\nStep 2: Testing getModelInstance method...');
console.log('Parameters:');
console.log(`- Model: ${TEST_MODEL}`);
console.log(`- API Key: ${TEST_API_KEY.substring(0, 4)}...`);
// This is the exact flow used in /api/llmcall
const modelInstance = provider.getModelInstance({
model: TEST_MODEL,
serverEnv: mockServerEnv,
apiKeys: mockApiKeys,
providerSettings: mockProviderSettings
});
console.log('✅ getModelInstance succeeded');
console.log(`Model instance type: ${typeof modelInstance}`);
console.log('\nStep 3: Testing generateText with the model instance...');
// This mimics the generateText call in /api/llmcall
const result = await generateText({
system: 'You are a helpful assistant.',
messages: [
{
role: 'user',
content: 'Hello, this is a test message from the BayerMGA test script.',
},
],
model: modelInstance,
maxTokens: 1000,
});
console.log('✅ generateText succeeded');
console.log('Response:');
console.log(result.text);
console.log('\nUsage:');
console.log(JSON.stringify(result.usage, null, 2));
console.log('\n✅ All tests passed successfully!');
} catch (error) {
console.error('\n❌ Test failed with error:');
console.error(`Error name: ${error.name}`);
console.error(`Error message: ${error.message}`);
// Log additional error details if available
if (error.cause) {
console.error('\nError cause:');
console.error(error.cause);
}
if (error.stack) {
console.error('\nStack trace:');
console.error(error.stack);
}
// Check for common error patterns
if (error.message.includes('API key')) {
console.error('\n🔑 This appears to be an API key issue. Make sure you have set a valid BAYER_MGA_API_KEY.');
} else if (error.message.includes('network') || error.message.includes('ECONNREFUSED')) {
console.error('\n🌐 This appears to be a network issue. Check your internet connection and the API base URL.');
} else if (error.message.includes('not found') || error.message.includes('undefined')) {
console.error('\n🔍 This appears to be a code issue. The provider or one of its methods might not be properly defined.');
}
// Try to extract HTTP error details if present
try {
if (error.cause && error.cause.response) {
console.error('\nHTTP Response Details:');
console.error(`Status: ${error.cause.response.status}`);
console.error(`Status Text: ${error.cause.response.statusText}`);
// Try to get response body
if (typeof error.cause.response.text === 'function') {
const responseText = await error.cause.response.text();
console.error('Response Body:', responseText);
}
}
} catch (e) {
console.error('Could not extract HTTP response details:', e.message);
}
}
}
// Run the test
console.log('Starting BayerMGA provider test...');
testBayerMGAProvider()
.then(() => console.log('Test completed.'))
.catch(err => console.error('Unhandled error in test:', err));