2025-12-22 22:36:36 +09:00
|
|
|
// Types for multi-provider model configuration
|
|
|
|
|
|
2026-10-12 14:51:55 +09:00
|
|
|
import { PROVIDERS, type ProviderName } from "@/lib/providers"
|
2026-10-12 14:27:50 +09:00
|
|
|
|
2026-10-12 19:42:09 +09:00
|
|
|
export type { ProviderName } from "@/lib/providers"
|
2025-12-22 22:36:36 +09:00
|
|
|
|
|
|
|
|
// Individual model configuration
|
|
|
|
|
export interface ModelConfig {
|
|
|
|
|
id: string // UUID for this model
|
|
|
|
|
modelId: string // e.g., "gpt-4o", "claude-sonnet-4-5"
|
|
|
|
|
validated?: boolean // Has this model been validated
|
|
|
|
|
validationError?: string // Error message if validation failed
|
2026-10-05 22:39:32 +09:00
|
|
|
validationWarning?: string // Passed, but e.g. did not call a tool
|
|
|
|
|
responseTime?: number // Milliseconds the last test took
|
2025-12-22 22:36:36 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Provider configuration
|
|
|
|
|
export interface ProviderConfig {
|
|
|
|
|
id: string // UUID for this provider config
|
|
|
|
|
provider: ProviderName
|
|
|
|
|
name?: string // Custom display name (e.g., "OpenAI Production")
|
|
|
|
|
apiKey: string
|
|
|
|
|
baseUrl?: string
|
|
|
|
|
// AWS Bedrock specific fields
|
|
|
|
|
awsAccessKeyId?: string
|
|
|
|
|
awsSecretAccessKey?: string
|
|
|
|
|
awsRegion?: string
|
|
|
|
|
awsSessionToken?: string // Optional, for temporary credentials
|
2026-01-12 14:42:32 -05:00
|
|
|
// Vertex AI specific fields
|
2026-01-14 03:03:30 -05:00
|
|
|
vertexApiKey?: string // Express Mode API key
|
2026-01-12 14:42:32 -05:00
|
|
|
|
2025-12-22 22:36:36 +09:00
|
|
|
models: ModelConfig[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The complete multi-model configuration
|
|
|
|
|
export interface MultiModelConfig {
|
|
|
|
|
version: 1
|
|
|
|
|
providers: ProviderConfig[]
|
|
|
|
|
selectedModelId?: string // Currently selected model's UUID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flattened model for dropdown display
|
|
|
|
|
export interface FlattenedModel {
|
2026-01-15 21:28:22 +05:30
|
|
|
id: string // Model config UUID or synthetic server ID (e.g., "server:provider:modelId")
|
2025-12-22 22:36:36 +09:00
|
|
|
modelId: string // Actual model ID
|
|
|
|
|
provider: ProviderName
|
|
|
|
|
providerLabel: string // Provider display name
|
|
|
|
|
apiKey: string
|
|
|
|
|
baseUrl?: string
|
|
|
|
|
// AWS Bedrock specific fields
|
|
|
|
|
awsAccessKeyId?: string
|
|
|
|
|
awsSecretAccessKey?: string
|
|
|
|
|
awsRegion?: string
|
|
|
|
|
awsSessionToken?: string
|
2026-01-12 14:42:32 -05:00
|
|
|
// Vertex AI specific fields
|
2026-01-14 03:03:30 -05:00
|
|
|
vertexApiKey?: string // Express Mode API key
|
2026-01-12 14:42:32 -05:00
|
|
|
|
2025-12-22 22:36:36 +09:00
|
|
|
validated?: boolean // Has this model been validated
|
2026-01-15 21:28:22 +05:30
|
|
|
// Source of this model config: user-defined (client) or server-defined
|
|
|
|
|
source?: "user" | "server"
|
|
|
|
|
// Whether this model is the server default (matches AI_MODEL env var)
|
|
|
|
|
isDefault?: boolean
|
2025-12-22 22:36:36 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to generate UUID
|
|
|
|
|
export function generateId(): string {
|
|
|
|
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create empty config
|
|
|
|
|
export function createEmptyConfig(): MultiModelConfig {
|
|
|
|
|
return {
|
|
|
|
|
version: 1,
|
|
|
|
|
providers: [],
|
|
|
|
|
selectedModelId: undefined,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new provider config
|
|
|
|
|
export function createProviderConfig(provider: ProviderName): ProviderConfig {
|
|
|
|
|
return {
|
|
|
|
|
id: generateId(),
|
|
|
|
|
provider,
|
|
|
|
|
apiKey: "",
|
2026-10-12 14:27:50 +09:00
|
|
|
baseUrl: PROVIDERS[provider].defaultBaseUrl,
|
2025-12-22 22:36:36 +09:00
|
|
|
models: [],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create new model config
|
|
|
|
|
export function createModelConfig(modelId: string): ModelConfig {
|
|
|
|
|
return {
|
|
|
|
|
id: generateId(),
|
|
|
|
|
modelId,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 21:28:22 +05:30
|
|
|
// Get all models as flattened list for dropdown (user-defined only)
|
2025-12-22 22:36:36 +09:00
|
|
|
export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
|
|
|
|
|
const models: FlattenedModel[] = []
|
|
|
|
|
|
|
|
|
|
for (const provider of config.providers) {
|
|
|
|
|
// Use custom name if provided, otherwise use default provider label
|
|
|
|
|
const providerLabel =
|
2026-10-12 14:27:50 +09:00
|
|
|
provider.name || PROVIDERS[provider.provider].label
|
2025-12-22 22:36:36 +09:00
|
|
|
|
|
|
|
|
for (const model of provider.models) {
|
|
|
|
|
models.push({
|
|
|
|
|
id: model.id,
|
|
|
|
|
modelId: model.modelId,
|
|
|
|
|
provider: provider.provider,
|
|
|
|
|
providerLabel,
|
|
|
|
|
apiKey: provider.apiKey,
|
|
|
|
|
baseUrl: provider.baseUrl,
|
|
|
|
|
// AWS Bedrock fields
|
|
|
|
|
awsAccessKeyId: provider.awsAccessKeyId,
|
|
|
|
|
awsSecretAccessKey: provider.awsSecretAccessKey,
|
|
|
|
|
awsRegion: provider.awsRegion,
|
|
|
|
|
awsSessionToken: provider.awsSessionToken,
|
2026-01-12 14:42:32 -05:00
|
|
|
// Vertex AI fields
|
2026-01-14 03:03:30 -05:00
|
|
|
vertexApiKey: provider.vertexApiKey,
|
2026-01-12 14:42:32 -05:00
|
|
|
|
2025-12-22 22:36:36 +09:00
|
|
|
validated: model.validated,
|
2026-01-15 21:28:22 +05:30
|
|
|
source: "user",
|
|
|
|
|
isDefault: false,
|
2025-12-22 22:36:36 +09:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return models
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find model by ID
|
|
|
|
|
export function findModelById(
|
|
|
|
|
config: MultiModelConfig,
|
|
|
|
|
modelId: string,
|
|
|
|
|
): FlattenedModel | undefined {
|
|
|
|
|
return flattenModels(config).find((m) => m.id === modelId)
|
|
|
|
|
}
|
2026-10-05 22:39:32 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A base URL the way the SDKs expect it: no spaces, no trailing slash, and
|
|
|
|
|
* no endpoint path users often paste along (".../v1/chat/completions"),
|
|
|
|
|
* which the SDK would append a second time.
|
|
|
|
|
*/
|
|
|
|
|
export function normalizeBaseUrl(url: string): string {
|
|
|
|
|
return url
|
|
|
|
|
.trim()
|
|
|
|
|
.replace(/\/+$/, "")
|
|
|
|
|
.replace(/\/(?:chat\/completions|completions|messages|responses)$/, "")
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 09:37:05 +09:00
|
|
|
/**
|
|
|
|
|
* Ollama's native API root, which the SDK appends /chat to and the model
|
|
|
|
|
* list /tags. Users often enter the server address ("http://localhost:11434")
|
|
|
|
|
* or its OpenAI-compatible one (".../v1"); both get /api.
|
|
|
|
|
*/
|
|
|
|
|
export function ollamaApiUrl(baseUrl: string): string {
|
|
|
|
|
return `${normalizeBaseUrl(baseUrl).replace(/\/(?:api|v1)$/, "")}/api`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 22:39:32 +09:00
|
|
|
/** Where a chat request goes for a base URL, or null when the SDK decides */
|
|
|
|
|
export function chatRequestUrl(
|
|
|
|
|
provider: ProviderName,
|
|
|
|
|
baseUrl: string,
|
|
|
|
|
): string | null {
|
|
|
|
|
const url = normalizeBaseUrl(baseUrl)
|
|
|
|
|
if (!url) return null
|
2026-10-12 14:48:32 +09:00
|
|
|
const { sdk, ownRequestPath } = PROVIDERS[provider]
|
|
|
|
|
if (sdk === "anthropic") return `${url}/messages`
|
|
|
|
|
if (sdk === "ollama") return `${ollamaApiUrl(url)}/chat`
|
2026-10-05 22:39:32 +09:00
|
|
|
// These SDKs build their own paths (or, for MiniMax, pick the protocol
|
|
|
|
|
// from the URL)
|
2026-10-12 14:48:32 +09:00
|
|
|
return ownRequestPath ? null : `${url}/chat/completions`
|
2026-10-05 22:39:32 +09:00
|
|
|
}
|