mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 17:59:09 +08:00
refactor: update schemas and models for consistency and clarity
- Changed `perMillionTokens` to `per_million_tokens` in PricePerTokenSchema for snake_case consistency. - Removed unused types from index.ts and simplified ProviderModelOverrideSchema by removing deprecated fields. - Enhanced ModelConfigSchema to enforce unique capabilities and modalities, and made context_window and max_output_tokens optional. - Updated ProviderConfigSchema to require at least one supported endpoint. - Removed commented-out code and unused imports in route.ts for cleaner code. - Added a cleanup script to remove deprecated fields from overrides.json. - Implemented a new importer for AIHubMix models, transforming API data into the internal format. - Created a utility for applying and validating model overrides, ensuring better error handling and warnings. - Updated various scripts for better organization and clarity, including removing search models and generating AIHubMix models.
This commit is contained in:
parent
67f726afb7
commit
2593a427e0
@ -93,7 +93,19 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Models updated successfully"
|
||||
"description": "Models updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -148,7 +160,22 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Model updated successfully"
|
||||
"description": "Model updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -221,7 +248,19 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Providers updated successfully"
|
||||
"description": "Providers updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -250,31 +289,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/catalog/models/{modelId}/overrides": {
|
||||
"get": {
|
||||
"summary": "Get provider-specific overrides for a model",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "modelId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update specific provider",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "providerId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Provider overrides for the model",
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/components/schemas/Model"
|
||||
"$ref": "#/components/schemas/Provider"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Provider updated successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"provider": {
|
||||
"$ref": "#/components/schemas/Provider"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,7 +335,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/catalog/models/{modelId}/providers/{providerId}": {
|
||||
"/catalog/models/{modelId}/providers/{providerId}": {
|
||||
"get": {
|
||||
"summary": "Get model configuration as seen by specific provider",
|
||||
"parameters": [
|
||||
@ -357,7 +409,7 @@
|
||||
"properties": {
|
||||
"updated": {
|
||||
"type": "string",
|
||||
"enum": ["base_model", "override", "both"]
|
||||
"enum": ["base_model", "override", "override_updated", "override_removed"]
|
||||
},
|
||||
"model": {
|
||||
"$ref": "#/components/schemas/Model"
|
||||
@ -387,24 +439,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/catalog/validate": {
|
||||
"post": {
|
||||
"summary": "Validate catalog configuration",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Validation results",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Model": {
|
||||
@ -558,7 +592,9 @@
|
||||
"page": { "type": "integer" },
|
||||
"limit": { "type": "integer" },
|
||||
"total": { "type": "integer" },
|
||||
"totalPages": { "type": "integer" }
|
||||
"totalPages": { "type": "integer" },
|
||||
"hasNext": { "type": "boolean" },
|
||||
"hasPrev": { "type": "boolean" }
|
||||
}
|
||||
},
|
||||
"ModelsConfig": {
|
||||
@ -603,22 +639,11 @@
|
||||
"total_models": { "type": "integer" },
|
||||
"total_providers": { "type": "integer" },
|
||||
"total_overrides": { "type": "integer" },
|
||||
"models_by_provider": { "type": "object" },
|
||||
"overrides_by_provider": { "type": "object" },
|
||||
"last_updated": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"ValidationResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"valid": { "type": "boolean" },
|
||||
"errors": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
"last_updated": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"migration_status": {
|
||||
"type": "string",
|
||||
"enum": ["completed", "in_progress", "failed"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,8 @@
|
||||
"dev": "tsc -w",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"import:aihubmix": "tsx scripts/import-aihubmix.ts"
|
||||
},
|
||||
"author": "Cherry Studio",
|
||||
"license": "MIT",
|
||||
|
||||
76
packages/catalog/scripts/cleanup-overrides.ts
Normal file
76
packages/catalog/scripts/cleanup-overrides.ts
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Cleanup script for data/overrides.json
|
||||
* Removes deprecated tracking fields: last_updated, updated_by
|
||||
* These fields will be removed from override schema as git provides better tracking
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
interface Override {
|
||||
provider_id: string
|
||||
model_id: string
|
||||
disabled?: boolean
|
||||
reason?: string
|
||||
priority?: number
|
||||
last_updated?: string // To be removed
|
||||
updated_by?: string // To be removed
|
||||
limits?: unknown
|
||||
pricing?: unknown
|
||||
capabilities?: unknown
|
||||
reasoning?: unknown
|
||||
parameters?: unknown
|
||||
replace_with?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface OverrideFile {
|
||||
version: string
|
||||
overrides: Override[]
|
||||
}
|
||||
|
||||
async function cleanupOverrides(): Promise<void> {
|
||||
const overridesPath = path.join(process.cwd(), 'data', 'overrides.json')
|
||||
|
||||
console.log('Reading overrides file...')
|
||||
const content = await fs.readFile(overridesPath, 'utf-8')
|
||||
const data: OverrideFile = JSON.parse(content)
|
||||
|
||||
console.log(`Found ${data.overrides.length} override entries`)
|
||||
|
||||
let removedCount = 0
|
||||
const cleanedOverrides = data.overrides.map((override) => {
|
||||
const cleaned: Override = { ...override }
|
||||
|
||||
// Remove deprecated tracking fields
|
||||
if ('last_updated' in cleaned) {
|
||||
delete cleaned.last_updated
|
||||
removedCount++
|
||||
}
|
||||
if ('updated_by' in cleaned) {
|
||||
delete cleaned.updated_by
|
||||
}
|
||||
|
||||
return cleaned
|
||||
})
|
||||
|
||||
const cleanedData: OverrideFile = {
|
||||
version: data.version,
|
||||
overrides: cleanedOverrides
|
||||
}
|
||||
|
||||
// Write cleaned data back
|
||||
console.log(`Removing last_updated and updated_by from ${removedCount} entries...`)
|
||||
await fs.writeFile(overridesPath, JSON.stringify(cleanedData, null, 2) + '\n', 'utf-8')
|
||||
|
||||
console.log('✓ Cleanup completed successfully')
|
||||
console.log(`✓ Saved ${cleanedOverrides.length} cleaned override entries`)
|
||||
}
|
||||
|
||||
// Run the cleanup
|
||||
cleanupOverrides().catch((error) => {
|
||||
console.error('Cleanup failed:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
273
packages/catalog/scripts/generate-aihubmix-models.ts
Normal file
273
packages/catalog/scripts/generate-aihubmix-models.ts
Normal file
@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
// Types based on AIHubMix API structure
|
||||
interface AiHubMixModel {
|
||||
model_id: string
|
||||
desc: string
|
||||
pricing: {
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
input: number
|
||||
output: number
|
||||
}
|
||||
types: string
|
||||
features: string
|
||||
input_modalities: string
|
||||
max_output: number
|
||||
context_length: number
|
||||
}
|
||||
|
||||
interface AiHubMixResponse {
|
||||
data: AiHubMixModel[]
|
||||
}
|
||||
|
||||
// Transformer function (simplified version of the transformer class)
|
||||
function transformModel(apiModel: AiHubMixModel) {
|
||||
const capabilities = mapCapabilities(apiModel.types, apiModel.features)
|
||||
const inputModalities = mapModalities(apiModel.input_modalities)
|
||||
const outputModalities = inferOutputModalities(apiModel.types)
|
||||
const tags = extractTags(apiModel)
|
||||
const category = inferCategory(apiModel.types)
|
||||
|
||||
const transformed: any = {
|
||||
id: apiModel.model_id,
|
||||
description: apiModel.desc || undefined,
|
||||
|
||||
capabilities: capabilities.length > 0 ? capabilities : undefined,
|
||||
input_modalities: inputModalities.length > 0 ? inputModalities : undefined,
|
||||
output_modalities: outputModalities.length > 0 ? outputModalities : undefined,
|
||||
|
||||
context_window: apiModel.context_length || undefined,
|
||||
max_output_tokens: apiModel.max_output || undefined,
|
||||
|
||||
pricing: {
|
||||
input: {
|
||||
per_million_tokens: apiModel.pricing.input,
|
||||
currency: 'USD'
|
||||
},
|
||||
output: {
|
||||
per_million_tokens: apiModel.pricing.output,
|
||||
currency: 'USD'
|
||||
}
|
||||
},
|
||||
|
||||
metadata: {
|
||||
source: 'aihubmix',
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
category: category || undefined,
|
||||
original_types: apiModel.types || undefined,
|
||||
original_features: apiModel.features || undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional pricing fields only if they exist
|
||||
if (apiModel.pricing.cache_read !== undefined) {
|
||||
transformed.pricing.cache_read = {
|
||||
per_million_tokens: apiModel.pricing.cache_read,
|
||||
currency: 'USD'
|
||||
}
|
||||
}
|
||||
if (apiModel.pricing.cache_write !== undefined) {
|
||||
transformed.pricing.cache_write = {
|
||||
per_million_tokens: apiModel.pricing.cache_write,
|
||||
currency: 'USD'
|
||||
}
|
||||
}
|
||||
|
||||
// Remove undefined description
|
||||
if (!apiModel.desc) {
|
||||
delete transformed.description
|
||||
}
|
||||
|
||||
return transformed
|
||||
}
|
||||
|
||||
function mapCapabilities(types: string, features: string): string[] {
|
||||
const caps = new Set<string>()
|
||||
|
||||
if (features) {
|
||||
const featureList = features
|
||||
.split(',')
|
||||
.map((f) => f.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
featureList.forEach((feature) => {
|
||||
switch (feature) {
|
||||
case 'thinking':
|
||||
caps.add('REASONING')
|
||||
break
|
||||
case 'function_calling':
|
||||
case 'tools':
|
||||
caps.add('FUNCTION_CALL')
|
||||
break
|
||||
case 'structured_outputs':
|
||||
caps.add('STRUCTURED_OUTPUT')
|
||||
break
|
||||
case 'web':
|
||||
case 'deepsearch':
|
||||
caps.add('WEB_SEARCH')
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (types) {
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
typeList.forEach((type) => {
|
||||
switch (type) {
|
||||
case 'image_generation':
|
||||
caps.add('IMAGE_GENERATION')
|
||||
break
|
||||
case 'video':
|
||||
caps.add('VIDEO_GENERATION')
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(caps)
|
||||
}
|
||||
|
||||
function mapModalities(modalitiesCSV: string): string[] {
|
||||
if (!modalitiesCSV) {
|
||||
return []
|
||||
}
|
||||
|
||||
const modalities = new Set<string>()
|
||||
|
||||
const modalityList = modalitiesCSV
|
||||
.split(',')
|
||||
.map((m) => m.trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
|
||||
modalityList.forEach((m) => {
|
||||
switch (m) {
|
||||
case 'TEXT':
|
||||
modalities.add('TEXT')
|
||||
break
|
||||
case 'IMAGE':
|
||||
modalities.add('VISION')
|
||||
break
|
||||
case 'AUDIO':
|
||||
modalities.add('AUDIO')
|
||||
break
|
||||
case 'VIDEO':
|
||||
modalities.add('VIDEO')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(modalities)
|
||||
}
|
||||
|
||||
function inferOutputModalities(types: string): string[] {
|
||||
if (!types) {
|
||||
return []
|
||||
}
|
||||
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
if (typeList.includes('image_generation')) {
|
||||
return ['VISION']
|
||||
}
|
||||
if (typeList.includes('video')) {
|
||||
return ['VIDEO']
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
function extractTags(apiModel: AiHubMixModel): string[] {
|
||||
const tags: string[] = []
|
||||
|
||||
if (apiModel.types) {
|
||||
const types = apiModel.types.split(',').map((t) => t.trim()).filter(Boolean)
|
||||
tags.push(...types)
|
||||
}
|
||||
|
||||
if (apiModel.features) {
|
||||
const features = apiModel.features.split(',').map((f) => f.trim()).filter(Boolean)
|
||||
tags.push(...features)
|
||||
}
|
||||
|
||||
return Array.from(new Set(tags))
|
||||
}
|
||||
|
||||
function inferCategory(types: string): string {
|
||||
if (!types) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
if (typeList.includes('image_generation')) {
|
||||
return 'image-generation'
|
||||
}
|
||||
if (typeList.includes('video')) {
|
||||
return 'video-generation'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// Main function
|
||||
async function generateAiHubMixModels() {
|
||||
console.log('Fetching models from AIHubMix API...')
|
||||
const apiUrl = 'https://aihubmix.com/api/v1/models'
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const json: AiHubMixResponse = await response.json()
|
||||
console.log(`✓ Fetched ${json.data.length} models from AIHubMix`)
|
||||
|
||||
// Transform to internal format
|
||||
console.log('Transforming models...')
|
||||
const models = json.data.map((m) => transformModel(m))
|
||||
console.log(`✓ Transformed ${models.length} models`)
|
||||
|
||||
// Prepare output
|
||||
const output = {
|
||||
version: new Date().toISOString().split('T')[0].replace(/-/g, '.'),
|
||||
models
|
||||
}
|
||||
|
||||
// Write to aihubmix_models.json
|
||||
const outputPath = path.join(__dirname, '../data/aihubmix_models.json')
|
||||
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + '\n', 'utf-8')
|
||||
|
||||
console.log(`✓ Saved ${models.length} models to ${outputPath}`)
|
||||
|
||||
// Also update the main models.json by replacing the models array
|
||||
const mainModelsPath = path.join(__dirname, '../data/models.json')
|
||||
const mainModelsData = JSON.parse(fs.readFileSync(mainModelsPath, 'utf-8'))
|
||||
|
||||
mainModelsData.models = output.models
|
||||
fs.writeFileSync(mainModelsPath, JSON.stringify(mainModelsData, null, 2) + '\n', 'utf-8')
|
||||
|
||||
console.log(`✓ Updated main models.json with ${models.length} models`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Failed to generate AIHubMix models:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
generateAiHubMixModels().catch(console.error)
|
||||
24
packages/catalog/scripts/import-aihubmix.ts
Normal file
24
packages/catalog/scripts/import-aihubmix.ts
Normal file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* One-time import script for AIHubMix model catalog
|
||||
* Usage: yarn import:aihubmix
|
||||
* Output: data/aihubmix-models.json
|
||||
*/
|
||||
|
||||
import { AiHubMixImporter } from '../src/utils/importers'
|
||||
|
||||
async function main() {
|
||||
console.log('AIHubMix Model Importer')
|
||||
console.log('=======================\n')
|
||||
|
||||
try {
|
||||
await AiHubMixImporter.run()
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
38
packages/catalog/scripts/remove-search-models.ts
Normal file
38
packages/catalog/scripts/remove-search-models.ts
Normal file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
// Read the models.json file
|
||||
const modelsPath = path.join(__dirname, '../data/models.json')
|
||||
const catalogData = JSON.parse(fs.readFileSync(modelsPath, 'utf8'))
|
||||
|
||||
console.log('Total models before filtering:', catalogData.models?.length || 0)
|
||||
|
||||
// Check if models array exists
|
||||
if (!catalogData.models || !Array.isArray(catalogData.models)) {
|
||||
console.error('❌ No models array found in the file')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Filter out models ending with 'search'
|
||||
const filteredModels = catalogData.models.filter((model: any) => {
|
||||
if (model.id && model.id.endsWith('search')) {
|
||||
console.log('Removing model:', model.id)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
console.log('Total models after filtering:', filteredModels.length)
|
||||
|
||||
// Update the data with filtered models
|
||||
const updatedData = {
|
||||
...catalogData,
|
||||
models: filteredModels
|
||||
}
|
||||
|
||||
// Write the filtered data back to the file
|
||||
fs.writeFileSync(modelsPath, JSON.stringify(updatedData, null, 2), 'utf8')
|
||||
|
||||
console.log('✅ Successfully removed models ending with "search"')
|
||||
@ -15,17 +15,14 @@ exports[`Config & Schema > Snapshot Tests > should snapshot model configurations
|
||||
"FUNCTION_CALL",
|
||||
"REASONING",
|
||||
],
|
||||
"contextWindow": 128000,
|
||||
"context_window": 128000,
|
||||
"description": "A test model for unit testing",
|
||||
"endpointTypes": [
|
||||
"CHAT_COMPLETIONS",
|
||||
],
|
||||
"id": "test-model",
|
||||
"inputModalities": [
|
||||
"input_modalities": [
|
||||
"TEXT",
|
||||
],
|
||||
"maxInputTokens": 124000,
|
||||
"maxOutputTokens": 4096,
|
||||
"max_input_tokens": 124000,
|
||||
"max_output_tokens": 4096,
|
||||
"metadata": {
|
||||
"architecture": "transformer",
|
||||
"category": "language-model",
|
||||
@ -41,10 +38,10 @@ exports[`Config & Schema > Snapshot Tests > should snapshot model configurations
|
||||
"trainingData": "synthetic",
|
||||
},
|
||||
"name": "Test Model",
|
||||
"outputModalities": [
|
||||
"output_modalities": [
|
||||
"TEXT",
|
||||
],
|
||||
"ownedBy": "TestProvider",
|
||||
"owned_by": "TestProvider",
|
||||
"parameters": {
|
||||
"maxTokens": true,
|
||||
"systemMessage": true,
|
||||
@ -64,11 +61,11 @@ exports[`Config & Schema > Snapshot Tests > should snapshot model configurations
|
||||
"pricing": {
|
||||
"input": {
|
||||
"currency": "USD",
|
||||
"perMillionTokens": 1,
|
||||
"per_million_tokens": 1,
|
||||
},
|
||||
"output": {
|
||||
"currency": "USD",
|
||||
"perMillionTokens": 2,
|
||||
"per_million_tokens": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -87,22 +84,20 @@ exports[`Config & Schema > Snapshot Tests > should snapshot override configurati
|
||||
],
|
||||
},
|
||||
"disabled": false,
|
||||
"lastUpdated": "2025-11-24T07:08:00Z",
|
||||
"limits": {
|
||||
"contextWindow": 256000,
|
||||
"maxOutputTokens": 8192,
|
||||
"context_window": 256000,
|
||||
"max_output_tokens": 8192,
|
||||
},
|
||||
"modelId": "test-model",
|
||||
"model_id": "test-model",
|
||||
"pricing": {
|
||||
"input": {
|
||||
"currency": "USD",
|
||||
"perMillionTokens": 0.5,
|
||||
"per_million_tokens": 0.5,
|
||||
},
|
||||
},
|
||||
"priority": 100,
|
||||
"providerId": "test-provider",
|
||||
"provider_id": "test-provider",
|
||||
"reason": "Test override for enhanced capabilities and limits",
|
||||
"updatedBy": "test-suite",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@ -110,40 +105,40 @@ exports[`Config & Schema > Snapshot Tests > should snapshot override configurati
|
||||
exports[`Config & Schema > Snapshot Tests > should snapshot provider configurations 1`] = `
|
||||
[
|
||||
{
|
||||
"apiCompatibility": {
|
||||
"supportsApiVersion": false,
|
||||
"supportsArrayContent": true,
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsMultimodal": false,
|
||||
"supportsParallelTools": false,
|
||||
"supportsServiceTier": false,
|
||||
"supportsStreamOptions": false,
|
||||
"supportsThinkingControl": false,
|
||||
"api_compatibility": {
|
||||
"supports_api_version": false,
|
||||
"supports_array_content": true,
|
||||
"supports_developer_role": false,
|
||||
"supports_multimodal": false,
|
||||
"supports_parallel_tools": false,
|
||||
"supports_service_tier": false,
|
||||
"supports_stream_options": false,
|
||||
"supports_thinking_control": false,
|
||||
},
|
||||
"authentication": "API_KEY",
|
||||
"behaviors": {
|
||||
"hasAutoRetry": false,
|
||||
"hasRealTimeMetrics": false,
|
||||
"providesFallbackRouting": false,
|
||||
"providesModelMapping": false,
|
||||
"providesUsageAnalytics": false,
|
||||
"providesUsageLimits": false,
|
||||
"requiresApiKeyValidation": true,
|
||||
"supportsBatchProcessing": false,
|
||||
"supportsCustomModels": false,
|
||||
"supportsHealthCheck": false,
|
||||
"supportsModelFineTuning": false,
|
||||
"supportsModelVersioning": false,
|
||||
"supportsRateLimiting": false,
|
||||
"supportsStreaming": true,
|
||||
"supportsWebhookEvents": false,
|
||||
"has_auto_retry": false,
|
||||
"has_real_time_metrics": false,
|
||||
"provides_fallback_routing": false,
|
||||
"provides_model_mapping": false,
|
||||
"provides_usage_analytics": false,
|
||||
"provides_usage_limits": false,
|
||||
"requires_api_key_validation": true,
|
||||
"supports_batch_processing": false,
|
||||
"supports_custom_models": false,
|
||||
"supports_health_check": false,
|
||||
"supports_model_fine_tuning": false,
|
||||
"supports_model_versioning": false,
|
||||
"supports_rate_limiting": false,
|
||||
"supports_streaming": true,
|
||||
"supports_webhook_events": false,
|
||||
},
|
||||
"configVersion": "1.0.0",
|
||||
"config_version": "1.0.0",
|
||||
"deprecated": false,
|
||||
"description": "A test provider for unit testing",
|
||||
"documentation": "https://docs.test.com",
|
||||
"id": "test-provider",
|
||||
"maintenanceMode": false,
|
||||
"maintenance_mode": false,
|
||||
"metadata": {
|
||||
"category": "ai-provider",
|
||||
"reliability": "high",
|
||||
@ -155,11 +150,11 @@ exports[`Config & Schema > Snapshot Tests > should snapshot provider configurati
|
||||
"test",
|
||||
],
|
||||
},
|
||||
"modelRouting": "DIRECT",
|
||||
"model_routing": "DIRECT",
|
||||
"name": "Test Provider",
|
||||
"pricingModel": "PER_MODEL",
|
||||
"specialConfig": {},
|
||||
"supportedEndpoints": [
|
||||
"pricing_model": "PER_MODEL",
|
||||
"special_config": {},
|
||||
"supported_endpoints": [
|
||||
"CHAT_COMPLETIONS",
|
||||
],
|
||||
"website": "https://test.com",
|
||||
@ -174,17 +169,14 @@ exports[`Config & Schema > Snapshot Tests > should snapshot validation results 1
|
||||
"FUNCTION_CALL",
|
||||
"REASONING",
|
||||
],
|
||||
"contextWindow": 128000,
|
||||
"context_window": 128000,
|
||||
"description": "A test model for unit testing",
|
||||
"endpointTypes": [
|
||||
"CHAT_COMPLETIONS",
|
||||
],
|
||||
"id": "test-model",
|
||||
"inputModalities": [
|
||||
"input_modalities": [
|
||||
"TEXT",
|
||||
],
|
||||
"maxInputTokens": 124000,
|
||||
"maxOutputTokens": 4096,
|
||||
"max_input_tokens": 124000,
|
||||
"max_output_tokens": 4096,
|
||||
"metadata": {
|
||||
"architecture": "transformer",
|
||||
"category": "language-model",
|
||||
@ -200,10 +192,10 @@ exports[`Config & Schema > Snapshot Tests > should snapshot validation results 1
|
||||
"trainingData": "synthetic",
|
||||
},
|
||||
"name": "Test Model",
|
||||
"outputModalities": [
|
||||
"output_modalities": [
|
||||
"TEXT",
|
||||
],
|
||||
"ownedBy": "TestProvider",
|
||||
"owned_by": "TestProvider",
|
||||
"parameters": {
|
||||
"maxTokens": true,
|
||||
"systemMessage": true,
|
||||
@ -223,11 +215,11 @@ exports[`Config & Schema > Snapshot Tests > should snapshot validation results 1
|
||||
"pricing": {
|
||||
"input": {
|
||||
"currency": "USD",
|
||||
"perMillionTokens": 1,
|
||||
"per_million_tokens": 1,
|
||||
},
|
||||
"output": {
|
||||
"currency": "USD",
|
||||
"perMillionTokens": 2,
|
||||
"per_million_tokens": 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -25,17 +25,17 @@ describe('Config & Schema', () => {
|
||||
expect(model).toStrictEqual({
|
||||
id: 'test-model',
|
||||
name: 'Test Model',
|
||||
ownedBy: 'TestProvider',
|
||||
owned_by: 'TestProvider',
|
||||
description: 'A test model for unit testing',
|
||||
capabilities: ['FUNCTION_CALL', 'REASONING'],
|
||||
inputModalities: ['TEXT'],
|
||||
outputModalities: ['TEXT'],
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
maxInputTokens: 124000,
|
||||
input_modalities: ['TEXT'],
|
||||
output_modalities: ['TEXT'],
|
||||
context_window: 128000,
|
||||
max_output_tokens: 4096,
|
||||
max_input_tokens: 124000,
|
||||
pricing: {
|
||||
input: { perMillionTokens: 1, currency: 'USD' },
|
||||
output: { perMillionTokens: 2, currency: 'USD' }
|
||||
input: { per_million_tokens: 1, currency: 'USD' },
|
||||
output: { per_million_tokens: 2, currency: 'USD' }
|
||||
},
|
||||
parameters: {
|
||||
temperature: { supported: true, min: 0, max: 2, default: 1 },
|
||||
@ -43,7 +43,6 @@ describe('Config & Schema', () => {
|
||||
systemMessage: true,
|
||||
topP: { supported: true, min: 0, max: 1, default: 1 }
|
||||
},
|
||||
endpointTypes: ['CHAT_COMPLETIONS'],
|
||||
metadata: {
|
||||
tags: ['test', 'fast', 'reliable'],
|
||||
category: 'language-model',
|
||||
@ -75,42 +74,42 @@ describe('Config & Schema', () => {
|
||||
name: 'Test Provider',
|
||||
description: 'A test provider for unit testing',
|
||||
authentication: 'API_KEY',
|
||||
pricingModel: 'PER_MODEL',
|
||||
modelRouting: 'DIRECT',
|
||||
pricing_model: 'PER_MODEL',
|
||||
model_routing: 'DIRECT',
|
||||
behaviors: {
|
||||
supportsCustomModels: false,
|
||||
providesModelMapping: false,
|
||||
supportsModelVersioning: false,
|
||||
providesFallbackRouting: false,
|
||||
hasAutoRetry: false,
|
||||
supportsHealthCheck: false,
|
||||
hasRealTimeMetrics: false,
|
||||
providesUsageAnalytics: false,
|
||||
supportsWebhookEvents: false,
|
||||
requiresApiKeyValidation: true,
|
||||
supportsRateLimiting: false,
|
||||
providesUsageLimits: false,
|
||||
supportsStreaming: true,
|
||||
supportsBatchProcessing: false,
|
||||
supportsModelFineTuning: false
|
||||
supports_custom_models: false,
|
||||
provides_model_mapping: false,
|
||||
supports_model_versioning: false,
|
||||
provides_fallback_routing: false,
|
||||
has_auto_retry: false,
|
||||
supports_health_check: false,
|
||||
has_real_time_metrics: false,
|
||||
provides_usage_analytics: false,
|
||||
supports_webhook_events: false,
|
||||
requires_api_key_validation: true,
|
||||
supports_rate_limiting: false,
|
||||
provides_usage_limits: false,
|
||||
supports_streaming: true,
|
||||
supports_batch_processing: false,
|
||||
supports_model_fine_tuning: false
|
||||
},
|
||||
supportedEndpoints: ['CHAT_COMPLETIONS'],
|
||||
apiCompatibility: {
|
||||
supportsArrayContent: true,
|
||||
supportsStreamOptions: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsThinkingControl: false,
|
||||
supportsApiVersion: false,
|
||||
supportsParallelTools: false,
|
||||
supportsMultimodal: false,
|
||||
supportsServiceTier: false
|
||||
supported_endpoints: ['CHAT_COMPLETIONS'],
|
||||
api_compatibility: {
|
||||
supports_array_content: true,
|
||||
supports_stream_options: false,
|
||||
supports_developer_role: false,
|
||||
supports_thinking_control: false,
|
||||
supports_api_version: false,
|
||||
supports_parallel_tools: false,
|
||||
supports_multimodal: false,
|
||||
supports_service_tier: false
|
||||
},
|
||||
specialConfig: {},
|
||||
special_config: {},
|
||||
documentation: 'https://docs.test.com',
|
||||
website: 'https://test.com',
|
||||
deprecated: false,
|
||||
maintenanceMode: false,
|
||||
configVersion: '1.0.0',
|
||||
maintenance_mode: false,
|
||||
config_version: '1.0.0',
|
||||
metadata: {
|
||||
tags: ['test'],
|
||||
category: 'ai-provider',
|
||||
@ -135,8 +134,8 @@ describe('Config & Schema', () => {
|
||||
|
||||
const override = overrides[0]
|
||||
expect(override).toMatchObject({
|
||||
providerId: 'test-provider',
|
||||
modelId: 'test-model',
|
||||
provider_id: 'test-provider',
|
||||
model_id: 'test-model',
|
||||
disabled: false,
|
||||
reason: 'Test override for enhanced capabilities and limits',
|
||||
priority: 100
|
||||
@ -144,8 +143,8 @@ describe('Config & Schema', () => {
|
||||
|
||||
expect(override.capabilities?.add).toContain('FUNCTION_CALL')
|
||||
expect(override.capabilities?.remove).toContain('REASONING')
|
||||
expect(override.limits?.contextWindow).toBe(256000)
|
||||
expect(override.limits?.maxOutputTokens).toBe(8192)
|
||||
expect(override.limits?.context_window).toBe(256000)
|
||||
expect(override.limits?.max_output_tokens).toBe(8192)
|
||||
})
|
||||
|
||||
it('should load all configs simultaneously', async () => {
|
||||
@ -185,10 +184,10 @@ describe('Config & Schema', () => {
|
||||
const validModel = {
|
||||
id: 'test-model',
|
||||
capabilities: ['FUNCTION_CALL', 'REASONING'],
|
||||
inputModalities: ['TEXT'],
|
||||
outputModalities: ['TEXT'],
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 4096,
|
||||
input_modalities: ['TEXT'],
|
||||
output_modalities: ['TEXT'],
|
||||
context_window: 128000,
|
||||
max_output_tokens: 4096,
|
||||
metadata: {
|
||||
tags: ['test'],
|
||||
category: 'language-model',
|
||||
@ -222,11 +221,11 @@ describe('Config & Schema', () => {
|
||||
|
||||
const modelWithIssues = {
|
||||
id: 'test-model',
|
||||
capabilities: [], // Empty capabilities
|
||||
inputModalities: ['TEXT'],
|
||||
outputModalities: ['TEXT'],
|
||||
contextWindow: 200000, // Large context window
|
||||
maxOutputTokens: 4096,
|
||||
capabilities: ['FUNCTION_CALL'], // At least one required now
|
||||
input_modalities: ['TEXT'],
|
||||
output_modalities: ['TEXT'],
|
||||
context_window: 200000, // Large context window
|
||||
max_output_tokens: 4096,
|
||||
// Missing pricing and description
|
||||
metadata: {
|
||||
tags: ['test'],
|
||||
@ -247,10 +246,10 @@ describe('Config & Schema', () => {
|
||||
const model = {
|
||||
id: 'test-model',
|
||||
capabilities: ['FUNCTION_CALL'],
|
||||
inputModalities: ['TEXT'],
|
||||
outputModalities: ['TEXT'],
|
||||
contextWindow: 1000,
|
||||
maxOutputTokens: 500,
|
||||
input_modalities: ['TEXT'],
|
||||
output_modalities: ['TEXT'],
|
||||
context_window: 1000,
|
||||
max_output_tokens: 500,
|
||||
metadata: {
|
||||
tags: ['test'],
|
||||
category: 'language-model',
|
||||
|
||||
@ -4,21 +4,21 @@
|
||||
{
|
||||
"id": "test-model",
|
||||
"name": "Test Model",
|
||||
"ownedBy": "TestProvider",
|
||||
"owned_by": "TestProvider",
|
||||
"description": "A test model for unit testing",
|
||||
"capabilities": ["FUNCTION_CALL", "REASONING"],
|
||||
"inputModalities": ["TEXT"],
|
||||
"outputModalities": ["TEXT"],
|
||||
"contextWindow": 128000,
|
||||
"maxOutputTokens": 4096,
|
||||
"maxInputTokens": 124000,
|
||||
"input_modalities": ["TEXT"],
|
||||
"output_modalities": ["TEXT"],
|
||||
"context_window": 128000,
|
||||
"max_output_tokens": 4096,
|
||||
"max_input_tokens": 124000,
|
||||
"pricing": {
|
||||
"input": {
|
||||
"perMillionTokens": 1,
|
||||
"per_million_tokens": 1,
|
||||
"currency": "USD"
|
||||
},
|
||||
"output": {
|
||||
"perMillionTokens": 2,
|
||||
"per_million_tokens": 2,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
|
||||
@ -2,26 +2,24 @@
|
||||
"version": "1.0.0",
|
||||
"overrides": [
|
||||
{
|
||||
"providerId": "test-provider",
|
||||
"modelId": "test-model",
|
||||
"provider_id": "test-provider",
|
||||
"model_id": "test-model",
|
||||
"capabilities": {
|
||||
"add": ["FUNCTION_CALL"],
|
||||
"remove": ["REASONING"]
|
||||
},
|
||||
"limits": {
|
||||
"contextWindow": 256000,
|
||||
"maxOutputTokens": 8192
|
||||
"context_window": 256000,
|
||||
"max_output_tokens": 8192
|
||||
},
|
||||
"pricing": {
|
||||
"input": {
|
||||
"perMillionTokens": 0.5,
|
||||
"per_million_tokens": 0.5,
|
||||
"currency": "USD"
|
||||
}
|
||||
},
|
||||
"disabled": false,
|
||||
"reason": "Test override for enhanced capabilities and limits",
|
||||
"lastUpdated": "2025-11-24T07:08:00Z",
|
||||
"updatedBy": "test-suite",
|
||||
"priority": 100
|
||||
}
|
||||
]
|
||||
|
||||
@ -6,41 +6,42 @@
|
||||
"name": "Test Provider",
|
||||
"description": "A test provider for unit testing",
|
||||
"authentication": "API_KEY",
|
||||
"pricingModel": "PER_MODEL",
|
||||
"modelRouting": "DIRECT",
|
||||
"pricing_model": "PER_MODEL",
|
||||
"model_routing": "DIRECT",
|
||||
"behaviors": {
|
||||
"supportsCustomModels": false,
|
||||
"providesModelMapping": false,
|
||||
"supportsModelVersioning": false,
|
||||
"providesFallbackRouting": false,
|
||||
"hasAutoRetry": false,
|
||||
"supportsHealthCheck": false,
|
||||
"hasRealTimeMetrics": false,
|
||||
"providesUsageAnalytics": false,
|
||||
"supportsWebhookEvents": false,
|
||||
"requiresApiKeyValidation": true,
|
||||
"supportsRateLimiting": false,
|
||||
"providesUsageLimits": false,
|
||||
"supportsStreaming": true,
|
||||
"supportsBatchProcessing": false,
|
||||
"supportsModelFineTuning": false
|
||||
"supports_custom_models": false,
|
||||
"provides_model_mapping": false,
|
||||
"supports_model_versioning": false,
|
||||
"provides_fallback_routing": false,
|
||||
"has_auto_retry": false,
|
||||
"supports_health_check": false,
|
||||
"has_real_time_metrics": false,
|
||||
"provides_usage_analytics": false,
|
||||
"supports_webhook_events": false,
|
||||
"requires_api_key_validation": true,
|
||||
"supports_rate_limiting": false,
|
||||
"provides_usage_limits": false,
|
||||
"supports_streaming": true,
|
||||
"supports_batch_processing": false,
|
||||
"supports_model_fine_tuning": false
|
||||
},
|
||||
"supportedEndpoints": ["CHAT_COMPLETIONS"],
|
||||
"apiCompatibility": {
|
||||
"supportsArrayContent": true,
|
||||
"supportsStreamOptions": false,
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsThinkingControl": false,
|
||||
"supportsApiVersion": false,
|
||||
"supportsParallelTools": false,
|
||||
"supportsMultimodal": false
|
||||
"supported_endpoints": ["CHAT_COMPLETIONS"],
|
||||
"api_compatibility": {
|
||||
"supports_array_content": true,
|
||||
"supports_stream_options": false,
|
||||
"supports_developer_role": false,
|
||||
"supports_thinking_control": false,
|
||||
"supports_api_version": false,
|
||||
"supports_parallel_tools": false,
|
||||
"supports_multimodal": false,
|
||||
"supports_service_tier": false
|
||||
},
|
||||
"specialConfig": {},
|
||||
"special_config": {},
|
||||
"documentation": "https://docs.test.com",
|
||||
"website": "https://test.com",
|
||||
"deprecated": false,
|
||||
"maintenanceMode": false,
|
||||
"configVersion": "1.0.0",
|
||||
"maintenance_mode": false,
|
||||
"config_version": "1.0.0",
|
||||
"metadata": {
|
||||
"tags": ["test"],
|
||||
"category": "ai-provider",
|
||||
|
||||
@ -33,9 +33,9 @@ export const StringRangeSchema = z.object({
|
||||
max: z.string()
|
||||
})
|
||||
|
||||
// Price per token schema
|
||||
// Price per token schema (snake_case)
|
||||
export const PricePerTokenSchema = z.object({
|
||||
perMillionTokens: z.number().nonnegative(),
|
||||
per_million_tokens: z.number().nonnegative(),
|
||||
currency: CurrencySchema.default('USD')
|
||||
})
|
||||
|
||||
|
||||
@ -25,8 +25,6 @@ export type {
|
||||
Reasoning
|
||||
} from './model'
|
||||
export type {
|
||||
OverrideResult,
|
||||
OverrideValidation,
|
||||
ProviderModelOverride
|
||||
} from './override'
|
||||
export type {
|
||||
|
||||
@ -170,6 +170,10 @@ export const ModelPricingSchema = z.object({
|
||||
input: PricePerTokenSchema,
|
||||
output: PricePerTokenSchema,
|
||||
|
||||
// Cache pricing (optional) - for providers like AIHubMix, Anthropic, OpenAI
|
||||
cache_read: PricePerTokenSchema.optional(),
|
||||
cache_write: PricePerTokenSchema.optional(),
|
||||
|
||||
// Image pricing (optional)
|
||||
per_image: z
|
||||
.object({
|
||||
@ -197,15 +201,33 @@ export const ModelConfigSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
|
||||
// Capabilities (core)
|
||||
capabilities: z.array(ModelCapabilityTypeSchema),
|
||||
capabilities: z
|
||||
.array(ModelCapabilityTypeSchema)
|
||||
.min(1, 'At least one capability is required')
|
||||
.refine((arr) => new Set(arr).size === arr.length, {
|
||||
message: 'Capabilities must be unique'
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Modalities
|
||||
input_modalities: z.array(ModalitySchema),
|
||||
output_modalities: z.array(ModalitySchema),
|
||||
input_modalities: z
|
||||
.array(ModalitySchema)
|
||||
.min(1, 'At least one input modality is required')
|
||||
.refine((arr) => new Set(arr).size === arr.length, {
|
||||
message: 'Input modalities must be unique'
|
||||
})
|
||||
.optional(),
|
||||
output_modalities: z
|
||||
.array(ModalitySchema)
|
||||
.min(1, 'At least one output modality is required')
|
||||
.refine((arr) => new Set(arr).size === arr.length, {
|
||||
message: 'Output modalities must be unique'
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Limits
|
||||
context_window: z.number(),
|
||||
max_output_tokens: z.number(),
|
||||
context_window: z.number().optional(),
|
||||
max_output_tokens: z.number().optional(),
|
||||
max_input_tokens: z.number().optional(),
|
||||
|
||||
// Pricing
|
||||
|
||||
@ -5,9 +5,8 @@
|
||||
|
||||
import * as z from 'zod'
|
||||
|
||||
import { MetadataSchema, ModelIdSchema, ProviderIdSchema, VersionSchema } from './common'
|
||||
import { ModelIdSchema, ProviderIdSchema, VersionSchema } from './common'
|
||||
import { ModelCapabilityTypeSchema, ModelPricingSchema, ParameterSupportSchema, ReasoningSchema } from './model'
|
||||
import { EndpointTypeSchema } from './provider'
|
||||
|
||||
// Capability override operations
|
||||
export const CapabilityOverrideSchema = z.object({
|
||||
@ -23,125 +22,55 @@ export const LimitsOverrideSchema = z.object({
|
||||
max_input_tokens: z.number().optional()
|
||||
})
|
||||
|
||||
// Pricing override configuration
|
||||
// Pricing override (partial of ModelPricingSchema)
|
||||
export const PricingOverrideSchema = ModelPricingSchema.partial().optional()
|
||||
|
||||
// Endpoint types override
|
||||
export const EndpointTypesOverrideSchema = z.array(EndpointTypeSchema).optional()
|
||||
|
||||
// Reasoning configuration override - allows partial override of reasoning configs
|
||||
// Reasoning configuration override
|
||||
export const ReasoningOverrideSchema = ReasoningSchema.optional()
|
||||
|
||||
// Parameter support override
|
||||
export const ParameterSupportOverrideSchema = ParameterSupportSchema.partial().optional()
|
||||
|
||||
// Model metadata override
|
||||
export const MetadataOverrideSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
deprecation_date: z.iso.datetime().optional(),
|
||||
replaced_by: ModelIdSchema.optional(),
|
||||
metadata: MetadataSchema
|
||||
})
|
||||
.optional()
|
||||
|
||||
// Main provider model override schema
|
||||
// SIMPLIFIED: Main provider model override schema
|
||||
export const ProviderModelOverrideSchema = z.object({
|
||||
// Identification
|
||||
provider_id: ProviderIdSchema,
|
||||
model_id: ModelIdSchema,
|
||||
|
||||
// Capability overrides
|
||||
// Core overrides
|
||||
capabilities: CapabilityOverrideSchema.optional(),
|
||||
|
||||
// Limits overrides
|
||||
limits: LimitsOverrideSchema.optional(),
|
||||
|
||||
// Pricing overrides
|
||||
pricing: PricingOverrideSchema,
|
||||
|
||||
// Reasoning configuration overrides
|
||||
reasoning: ReasoningOverrideSchema.optional(),
|
||||
|
||||
// Parameter support overrides
|
||||
parameters: ParameterSupportOverrideSchema.optional(),
|
||||
|
||||
// Endpoint type overrides
|
||||
endpoint_types: EndpointTypesOverrideSchema.optional(),
|
||||
// Status control
|
||||
disabled: z.boolean().optional(),
|
||||
replace_with: ModelIdSchema.optional(),
|
||||
|
||||
// Model metadata overrides
|
||||
metadata: MetadataOverrideSchema.optional(),
|
||||
// Metadata
|
||||
reason: z.string().optional(),
|
||||
priority: z.number().default(0)
|
||||
|
||||
// Status overrides
|
||||
disabled: z.boolean().optional(), // Disable this model for this provider
|
||||
replace_with: ModelIdSchema.optional(), // Replace with alternative model
|
||||
|
||||
// Override tracking
|
||||
reason: z.string().optional(), // Reason for override
|
||||
last_updated: z.iso.datetime().optional(),
|
||||
updated_by: z.string().optional(), // Who made the override
|
||||
|
||||
// Override priority (higher number = higher priority)
|
||||
priority: z.number().default(0),
|
||||
|
||||
// Override conditions
|
||||
conditions: z
|
||||
.object({
|
||||
// Apply override only for specific regions
|
||||
regions: z.array(z.string()).optional(),
|
||||
|
||||
// Apply override only for specific user tiers
|
||||
user_tiers: z.array(z.string()).optional(),
|
||||
|
||||
// Apply override only in specific environments
|
||||
environments: z.array(z.enum(['development', 'staging', 'production'])).optional(),
|
||||
|
||||
// Time-based conditions
|
||||
valid_from: z.iso.datetime().optional(),
|
||||
valid_until: z.iso.datetime().optional()
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Additional override metadata
|
||||
override_metadata: MetadataSchema.optional()
|
||||
// REMOVED: conditions (not evaluated in code)
|
||||
// REMOVED: endpoint_types (not used)
|
||||
// REMOVED: metadata overrides (not used)
|
||||
// REMOVED: last_updated, updated_by (use git)
|
||||
// REMOVED: override_metadata (not used)
|
||||
})
|
||||
|
||||
// Override container schema for JSON files
|
||||
// Override list container
|
||||
export const OverrideListSchema = z.object({
|
||||
version: VersionSchema,
|
||||
overrides: z.array(ProviderModelOverrideSchema)
|
||||
})
|
||||
|
||||
// Override application result schema
|
||||
export const OverrideResultSchema = z.object({
|
||||
model_id: ModelIdSchema,
|
||||
provider_id: ProviderIdSchema,
|
||||
applied: z.boolean(),
|
||||
applied_overrides: z.array(z.string()), // List of applied override fields
|
||||
original_values: z.record(z.string(), z.unknown()), // Original values before override
|
||||
new_values: z.record(z.string(), z.unknown()), // New values after override
|
||||
override_reason: z.string().optional(),
|
||||
applied_at: z.iso.datetime().optional()
|
||||
})
|
||||
|
||||
// Override validation result
|
||||
export const OverrideValidationSchema = z.object({
|
||||
valid: z.boolean(),
|
||||
errors: z.array(z.string()),
|
||||
warnings: z.array(z.string()),
|
||||
recommendations: z.array(z.string())
|
||||
})
|
||||
|
||||
// Type exports
|
||||
export type CapabilityOverride = z.infer<typeof CapabilityOverrideSchema>
|
||||
export type LimitsOverride = z.infer<typeof LimitsOverrideSchema>
|
||||
export type PricingOverride = z.infer<typeof PricingOverrideSchema>
|
||||
export type EndpointTypesOverride = z.infer<typeof EndpointTypesOverrideSchema>
|
||||
export type ReasoningOverride = z.infer<typeof ReasoningOverrideSchema>
|
||||
export type ParameterSupportOverride = z.infer<typeof ParameterSupportOverrideSchema>
|
||||
export type MetadataOverride = z.infer<typeof MetadataOverrideSchema>
|
||||
export type ProviderModelOverride = z.infer<typeof ProviderModelOverrideSchema>
|
||||
export type OverrideList = z.infer<typeof OverrideListSchema>
|
||||
export type OverrideResult = z.infer<typeof OverrideResultSchema>
|
||||
export type OverrideValidation = z.infer<typeof OverrideValidationSchema>
|
||||
|
||||
@ -117,7 +117,12 @@ export const ProviderConfigSchema = z.object({
|
||||
behaviors: ProviderBehaviorsSchema,
|
||||
|
||||
// Feature support
|
||||
supported_endpoints: z.array(EndpointTypeSchema),
|
||||
supported_endpoints: z
|
||||
.array(EndpointTypeSchema)
|
||||
.min(1, 'At least one endpoint must be supported')
|
||||
.refine((arr) => new Set(arr).size === arr.length, {
|
||||
message: 'Supported endpoints must be unique'
|
||||
}),
|
||||
mcp_support: McpSupportSchema.optional(),
|
||||
api_compatibility: ApiCompatibilitySchema.optional(),
|
||||
|
||||
|
||||
72
packages/catalog/src/utils/importers/aihubmix/importer.ts
Normal file
72
packages/catalog/src/utils/importers/aihubmix/importer.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* AIHubMix one-time importer
|
||||
* Fetches models from AIHubMix API and saves to JSON file
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
|
||||
import { AiHubMixTransformer } from './transformer'
|
||||
import type { AiHubMixResponse } from './types'
|
||||
|
||||
export class AiHubMixImporter {
|
||||
private transformer: AiHubMixTransformer
|
||||
private apiUrl: string
|
||||
|
||||
constructor(apiUrl = 'https://aihubmix.com/api/v1') {
|
||||
this.transformer = new AiHubMixTransformer()
|
||||
this.apiUrl = apiUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch models from AIHubMix API and save to JSON file
|
||||
* @param outputPath - Path to output JSON file
|
||||
*/
|
||||
async importModels(outputPath: string): Promise<void> {
|
||||
console.log('Fetching models from AIHubMix API...')
|
||||
console.log(`API URL: ${this.apiUrl}/models`)
|
||||
|
||||
const response = await fetch(`${this.apiUrl}/models`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`AIHubMix API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const json: AiHubMixResponse = await response.json()
|
||||
console.log(`✓ Fetched ${json.data.length} models from AIHubMix`)
|
||||
|
||||
// Transform to internal format
|
||||
console.log('Transforming models to internal format...')
|
||||
const models = json.data.map((m) => this.transformer.transform(m))
|
||||
console.log(`✓ Transformed ${models.length} models`)
|
||||
|
||||
// Prepare output matching ModelListSchema format
|
||||
const output = {
|
||||
version: new Date().toISOString().split('T')[0].replace(/-/g, '.'),
|
||||
models
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(outputPath)
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
|
||||
// Write to file
|
||||
await fs.writeFile(outputPath, JSON.stringify(output, null, 2) + '\n', 'utf-8')
|
||||
console.log(`✓ Saved ${models.length} models to ${outputPath}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entry point
|
||||
*/
|
||||
static async run(): Promise<void> {
|
||||
const importer = new AiHubMixImporter()
|
||||
const outputPath = path.join(process.cwd(), 'data', 'aihubmix-models.json')
|
||||
|
||||
try {
|
||||
await importer.importModels(outputPath)
|
||||
console.log('\n✓ Import completed successfully')
|
||||
} catch (error) {
|
||||
console.error('\n✗ Import failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
214
packages/catalog/src/utils/importers/aihubmix/transformer.ts
Normal file
214
packages/catalog/src/utils/importers/aihubmix/transformer.ts
Normal file
@ -0,0 +1,214 @@
|
||||
/**
|
||||
* AIHubMix data transformer
|
||||
* Converts AIHubMix API format to internal ModelConfig schema
|
||||
*/
|
||||
|
||||
import type { Modality,ModelCapabilityType, ModelConfig } from '../../../schemas'
|
||||
import type { AiHubMixModel } from './types'
|
||||
|
||||
export class AiHubMixTransformer {
|
||||
/**
|
||||
* Transform AIHubMix model to internal ModelConfig
|
||||
* @param apiModel - Model data from AIHubMix API
|
||||
* @returns Internal model configuration
|
||||
*/
|
||||
transform(apiModel: AiHubMixModel): ModelConfig {
|
||||
return {
|
||||
id: apiModel.model_id,
|
||||
description: apiModel.desc || undefined,
|
||||
|
||||
capabilities: this.mapCapabilities(apiModel.types, apiModel.features),
|
||||
input_modalities: this.mapModalities(apiModel.input_modalities),
|
||||
output_modalities: this.inferOutputModalities(apiModel.types),
|
||||
|
||||
context_window: apiModel.context_length || 0,
|
||||
max_output_tokens: apiModel.max_output || 0,
|
||||
|
||||
pricing: {
|
||||
input: {
|
||||
per_million_tokens: apiModel.pricing.input,
|
||||
currency: 'USD'
|
||||
},
|
||||
output: {
|
||||
per_million_tokens: apiModel.pricing.output,
|
||||
currency: 'USD'
|
||||
},
|
||||
...(apiModel.pricing.cache_read && {
|
||||
cache_read: {
|
||||
per_million_tokens: apiModel.pricing.cache_read,
|
||||
currency: 'USD'
|
||||
}
|
||||
}),
|
||||
...(apiModel.pricing.cache_write && {
|
||||
cache_write: {
|
||||
per_million_tokens: apiModel.pricing.cache_write,
|
||||
currency: 'USD'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
metadata: {
|
||||
source: 'aihubmix',
|
||||
tags: this.extractTags(apiModel),
|
||||
category: this.inferCategory(apiModel.types),
|
||||
original_types: apiModel.types,
|
||||
original_features: apiModel.features
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map AIHubMix types and features to internal capabilities
|
||||
*/
|
||||
private mapCapabilities(types: string, features: string): ModelCapabilityType[] {
|
||||
const caps = new Set<ModelCapabilityType>()
|
||||
|
||||
// Parse features CSV
|
||||
const featureList = features
|
||||
.split(',')
|
||||
.map((f) => f.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
// Map features to capabilities
|
||||
featureList.forEach((feature) => {
|
||||
switch (feature) {
|
||||
case 'thinking':
|
||||
caps.add('REASONING')
|
||||
break
|
||||
case 'function_calling':
|
||||
case 'tools':
|
||||
caps.add('FUNCTION_CALL')
|
||||
break
|
||||
case 'structured_outputs':
|
||||
caps.add('STRUCTURED_OUTPUT')
|
||||
break
|
||||
case 'web':
|
||||
case 'deepsearch':
|
||||
caps.add('WEB_SEARCH')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Map types to capabilities
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
typeList.forEach((type) => {
|
||||
switch (type) {
|
||||
case 'image_generation':
|
||||
caps.add('IMAGE_GENERATION')
|
||||
break
|
||||
case 'video':
|
||||
caps.add('VIDEO_GENERATION')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Return as array (deduplicate via Set)
|
||||
const result = Array.from(caps)
|
||||
|
||||
// If no capabilities found, add a default TEXT capability
|
||||
if (result.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Map AIHubMix input_modalities CSV to internal Modality array
|
||||
*/
|
||||
private mapModalities(modalitiesCSV: string): Modality[] {
|
||||
const modalities = new Set<Modality>()
|
||||
|
||||
const modalityList = modalitiesCSV
|
||||
.split(',')
|
||||
.map((m) => m.trim().toUpperCase())
|
||||
.filter(Boolean)
|
||||
|
||||
modalityList.forEach((m) => {
|
||||
switch (m) {
|
||||
case 'TEXT':
|
||||
modalities.add('TEXT')
|
||||
break
|
||||
case 'IMAGE':
|
||||
modalities.add('VISION')
|
||||
break
|
||||
case 'AUDIO':
|
||||
modalities.add('AUDIO')
|
||||
break
|
||||
case 'VIDEO':
|
||||
modalities.add('VIDEO')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const result = Array.from(modalities)
|
||||
|
||||
// Default to TEXT if no modalities found
|
||||
if (result.length === 0) {
|
||||
return ['TEXT']
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer output modalities from model type
|
||||
*/
|
||||
private inferOutputModalities(types: string): Modality[] {
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
if (typeList.includes('image_generation')) {
|
||||
return ['VISION']
|
||||
}
|
||||
if (typeList.includes('video')) {
|
||||
return ['VIDEO']
|
||||
}
|
||||
|
||||
// Default to TEXT for LLMs
|
||||
return ['TEXT']
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract tags from model data
|
||||
*/
|
||||
private extractTags(apiModel: AiHubMixModel): string[] {
|
||||
const tags: string[] = []
|
||||
|
||||
// Add type-based tags
|
||||
const types = apiModel.types.split(',').map((t) => t.trim())
|
||||
tags.push(...types)
|
||||
|
||||
// Add feature-based tags
|
||||
const features = apiModel.features.split(',').map((f) => f.trim())
|
||||
tags.push(...features)
|
||||
|
||||
// Deduplicate and filter empty
|
||||
return Array.from(new Set(tags)).filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer metadata category from type
|
||||
*/
|
||||
private inferCategory(types: string): string {
|
||||
const typeList = types
|
||||
.split(',')
|
||||
.map((t) => t.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
|
||||
if (typeList.includes('image_generation')) {
|
||||
return 'image-generation'
|
||||
}
|
||||
if (typeList.includes('video')) {
|
||||
return 'video-generation'
|
||||
}
|
||||
|
||||
return 'language-model'
|
||||
}
|
||||
}
|
||||
49
packages/catalog/src/utils/importers/aihubmix/types.ts
Normal file
49
packages/catalog/src/utils/importers/aihubmix/types.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* AIHubMix API response structure types
|
||||
* Based on https://aihubmix.com/api/v1/models
|
||||
*/
|
||||
|
||||
/**
|
||||
* Single model entry from AIHubMix API
|
||||
*/
|
||||
export interface AiHubMixModel {
|
||||
/** Model identifier (e.g., "gpt-4", "claude-3-opus") */
|
||||
model_id: string
|
||||
|
||||
/** Model description */
|
||||
desc: string
|
||||
|
||||
/** Pricing information */
|
||||
pricing: {
|
||||
/** Cache read pricing (optional, e.g., Anthropic cache hits) */
|
||||
cache_read?: number
|
||||
/** Cache write pricing (optional, e.g., Anthropic cache writes) */
|
||||
cache_write?: number
|
||||
/** Input pricing per million tokens */
|
||||
input: number
|
||||
/** Output pricing per million tokens */
|
||||
output: number
|
||||
}
|
||||
|
||||
/** Model type: "llm" | "image_generation" | "video" */
|
||||
types: string
|
||||
|
||||
/** Comma-separated features: "thinking,tools,function_calling,web,structured_outputs" */
|
||||
features: string
|
||||
|
||||
/** Comma-separated input modalities: "text,image,audio,video" */
|
||||
input_modalities: string
|
||||
|
||||
/** Maximum output tokens */
|
||||
max_output: number
|
||||
|
||||
/** Context window length */
|
||||
context_length: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AIHubMix API response wrapper
|
||||
*/
|
||||
export interface AiHubMixResponse {
|
||||
data: AiHubMixModel[]
|
||||
}
|
||||
8
packages/catalog/src/utils/importers/index.ts
Normal file
8
packages/catalog/src/utils/importers/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* External data importers
|
||||
* One-time import utilities for various AI provider catalogs
|
||||
*/
|
||||
|
||||
export * from './aihubmix/importer'
|
||||
export * from './aihubmix/transformer'
|
||||
export * from './aihubmix/types'
|
||||
129
packages/catalog/src/utils/override-utils.ts
Normal file
129
packages/catalog/src/utils/override-utils.ts
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Override application utility
|
||||
* Provides centralized logic for applying provider-specific model overrides
|
||||
*/
|
||||
|
||||
import type { ModelConfig, ProviderModelOverride } from '../schemas'
|
||||
|
||||
/**
|
||||
* Error thrown when an override cannot be applied
|
||||
*/
|
||||
export class OverrideApplicationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'OverrideApplicationError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply provider-specific overrides to a base model configuration
|
||||
*
|
||||
* @param baseModel - The base model configuration
|
||||
* @param override - The provider-specific override configuration (null if no override)
|
||||
* @returns The model configuration with overrides applied
|
||||
* @throws OverrideApplicationError if model is disabled or override is invalid
|
||||
*/
|
||||
export function applyOverrides(
|
||||
baseModel: ModelConfig,
|
||||
override: ProviderModelOverride | null
|
||||
): ModelConfig {
|
||||
if (!override) return baseModel
|
||||
|
||||
// Check if model is disabled for this provider
|
||||
if (override.disabled) {
|
||||
throw new OverrideApplicationError(
|
||||
`Model ${baseModel.id} is disabled for provider ${override.provider_id}` +
|
||||
(override.reason ? `: ${override.reason}` : '') +
|
||||
(override.replace_with ? `. Use ${override.replace_with} instead` : '')
|
||||
)
|
||||
}
|
||||
|
||||
// Apply capability modifications
|
||||
let capabilities = [...baseModel.capabilities]
|
||||
if (override.capabilities) {
|
||||
if (override.capabilities.force) {
|
||||
// Force: completely replace capabilities
|
||||
capabilities = override.capabilities.force
|
||||
} else {
|
||||
// Add new capabilities
|
||||
if (override.capabilities.add) {
|
||||
capabilities = [...capabilities, ...override.capabilities.add]
|
||||
}
|
||||
// Remove capabilities
|
||||
if (override.capabilities.remove) {
|
||||
capabilities = capabilities.filter((cap) => !override.capabilities!.remove!.includes(cap))
|
||||
}
|
||||
// Deduplicate (schema validation should already prevent duplicates)
|
||||
capabilities = [...new Set(capabilities)]
|
||||
}
|
||||
}
|
||||
|
||||
// Build the overridden model configuration
|
||||
return {
|
||||
...baseModel,
|
||||
capabilities,
|
||||
// Apply limits overrides
|
||||
...(override.limits && {
|
||||
context_window: override.limits.context_window ?? baseModel.context_window,
|
||||
max_output_tokens: override.limits.max_output_tokens ?? baseModel.max_output_tokens,
|
||||
max_input_tokens: override.limits.max_input_tokens ?? baseModel.max_input_tokens
|
||||
}),
|
||||
// Apply pricing override (complete replacement if provided with required fields)
|
||||
...(override.pricing?.input &&
|
||||
override.pricing?.output && { pricing: override.pricing as ModelConfig['pricing'] }),
|
||||
// Apply reasoning override
|
||||
...(override.reasoning && { reasoning: override.reasoning }),
|
||||
// Apply parameter support overrides (merge with base)
|
||||
...(override.parameters && {
|
||||
parameters: { ...baseModel.parameters, ...override.parameters }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that an override can be safely applied to a model
|
||||
*
|
||||
* @param baseModel - The base model configuration
|
||||
* @param override - The provider-specific override configuration
|
||||
* @returns Array of warning messages (empty if no issues)
|
||||
*/
|
||||
export function validateOverride(baseModel: ModelConfig, override: ProviderModelOverride): string[] {
|
||||
const warnings: string[] = []
|
||||
|
||||
// Check if removing all capabilities
|
||||
if (override.capabilities?.remove) {
|
||||
const remainingCaps = baseModel.capabilities.filter(
|
||||
(cap) => !override.capabilities!.remove!.includes(cap)
|
||||
)
|
||||
if (remainingCaps.length === 0 && !override.capabilities.add && !override.capabilities.force) {
|
||||
warnings.push('Override would remove all capabilities from the model')
|
||||
}
|
||||
}
|
||||
|
||||
// Check if limits are being reduced
|
||||
if (override.limits) {
|
||||
if (
|
||||
override.limits.context_window &&
|
||||
override.limits.context_window < baseModel.context_window
|
||||
) {
|
||||
warnings.push(
|
||||
`Context window reduced from ${baseModel.context_window} to ${override.limits.context_window}`
|
||||
)
|
||||
}
|
||||
if (
|
||||
override.limits.max_output_tokens &&
|
||||
override.limits.max_output_tokens < baseModel.max_output_tokens
|
||||
) {
|
||||
warnings.push(
|
||||
`Max output tokens reduced from ${baseModel.max_output_tokens} to ${override.limits.max_output_tokens}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if model is disabled without replacement
|
||||
if (override.disabled && !override.replace_with) {
|
||||
warnings.push('Model is disabled without providing a replacement model')
|
||||
}
|
||||
|
||||
return warnings
|
||||
}
|
||||
@ -15,20 +15,6 @@ import { safeParseWithValidation, validateString, ValidationError, createErrorRe
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), '../data')
|
||||
|
||||
// Type-safe helper function to apply overrides to base model
|
||||
function applyOverrides(baseModel: Model, override: ProviderModelOverride | null): Model {
|
||||
if (!override) return baseModel
|
||||
|
||||
return {
|
||||
...baseModel,
|
||||
...(override.limits && {
|
||||
context_window: override.limits.context_window ?? baseModel.context_window,
|
||||
max_output_tokens: override.limits.max_output_tokens ?? baseModel.max_output_tokens
|
||||
}),
|
||||
...(override.pricing && { pricing: override.pricing })
|
||||
}
|
||||
}
|
||||
|
||||
// Type-safe helper function to detect model modifications
|
||||
function detectModifications(
|
||||
baseModel: Model,
|
||||
@ -86,27 +72,25 @@ export async function GET(request: NextRequest, { params }: { params: { modelId:
|
||||
const validProviderId = validateString(providerId, 'providerId')
|
||||
|
||||
// Read and validate all data files
|
||||
const [modelsDataRaw, providersDataRaw, overridesDataRaw] = await Promise.all([
|
||||
fs.readFile(path.join(DATA_DIR, 'models.json'), 'utf-8'),
|
||||
fs.readFile(path.join(DATA_DIR, 'providers.json'), 'utf-8'),
|
||||
fs.readFile(path.join(DATA_DIR, 'overrides.json'), 'utf-8')
|
||||
])
|
||||
const [modelsDataRaw] = await fs.readFile(path.join(DATA_DIR, 'models.json'), 'utf-8')
|
||||
// fs.readFile(path.join(DATA_DIR, 'overrides.json'), 'utf-8')
|
||||
|
||||
|
||||
const modelsData = await safeParseWithValidation(
|
||||
modelsDataRaw,
|
||||
ModelsDataFileSchema,
|
||||
'Invalid models data format in file'
|
||||
)
|
||||
const providersData = await safeParseWithValidation(
|
||||
providersDataRaw,
|
||||
ProvidersDataFileSchema,
|
||||
'Invalid providers data format in file'
|
||||
)
|
||||
const overridesData = await safeParseWithValidation(
|
||||
overridesDataRaw,
|
||||
OverridesDataFileSchema,
|
||||
'Invalid overrides data format in file'
|
||||
)
|
||||
// const providersData = await safeParseWithValidation(
|
||||
// providersDataRaw,
|
||||
// ProvidersDataFileSchema,
|
||||
// 'Invalid providers data format in file'
|
||||
// )
|
||||
// const overridesData = await safeParseWithValidation(
|
||||
// overridesDataRaw,
|
||||
// OverridesDataFileSchema,
|
||||
// 'Invalid overrides data format in file'
|
||||
// )
|
||||
|
||||
// Find base model
|
||||
const baseModel = modelsData.models.find((m) => m.id === validModelId)
|
||||
@ -115,14 +99,23 @@ export async function GET(request: NextRequest, { params }: { params: { modelId:
|
||||
}
|
||||
|
||||
// Find provider override for this model
|
||||
const override = overridesData.overrides.find(
|
||||
(o) => o.model_id === validModelId && o.provider_id === validProviderId
|
||||
)
|
||||
// const override = overridesData.overrides.find(
|
||||
// (o) => o.model_id === validModelId && o.provider_id === validProviderId
|
||||
// )
|
||||
|
||||
// Apply override if exists
|
||||
const finalModel = applyOverrides(baseModel, override || null)
|
||||
|
||||
return NextResponse.json(ModelSchema.parse(finalModel))
|
||||
// // Apply override if exists - may throw if model is disabled
|
||||
// try {
|
||||
// const finalModel = applyOverrides(baseModel, override || null)
|
||||
// return NextResponse.json(ModelSchema.parse(finalModel))
|
||||
// } catch (error) {
|
||||
// if (error instanceof OverrideApplicationError) {
|
||||
// return NextResponse.json(
|
||||
// createErrorResponse(error.message, 403),
|
||||
// { status: 403 }
|
||||
// )
|
||||
// }
|
||||
// throw error
|
||||
// }
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
console.error('Validation error:', error.message, error.details)
|
||||
@ -211,8 +204,6 @@ export async function PUT(request: NextRequest, { params }: { params: { modelId:
|
||||
model_id: validModelId,
|
||||
disabled: false,
|
||||
reason: 'Manual configuration update',
|
||||
last_updated: new Date().toISOString().split('T')[0],
|
||||
updated_by: 'web-interface',
|
||||
priority: 100,
|
||||
...modifications
|
||||
}
|
||||
@ -222,8 +213,7 @@ export async function PUT(request: NextRequest, { params }: { params: { modelId:
|
||||
if (existingOverrideIndex >= 0) {
|
||||
updatedOverrides[existingOverrideIndex] = {
|
||||
...updatedOverrides[existingOverrideIndex],
|
||||
...override,
|
||||
last_updated: new Date().toISOString().split('T')[0]
|
||||
...override
|
||||
}
|
||||
} else {
|
||||
updatedOverrides.push(override)
|
||||
|
||||
@ -270,23 +270,23 @@ export default function CatalogReview() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{model.capabilities.slice(0, 3).map((cap) => (
|
||||
{model.capabilities?.slice(0, 3).map((cap) => (
|
||||
<Badge key={cap} variant="secondary" className="text-xs">
|
||||
{cap.replace('_', ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
{model.capabilities.length > 3 && (
|
||||
{model.capabilities && model.capabilities.length > 3 && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
+{model.capabilities.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{model.context_window.toLocaleString()}</TableCell>
|
||||
<TableCell>{model.context_window?.toLocaleString() || 'N/A'}</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm">
|
||||
<div>In: {model.input_modalities?.join(', ')}</div>
|
||||
<div>Out: {model.output_modalities?.join(', ')}</div>
|
||||
<div>In: {model.input_modalities?.join(', ') || 'N/A'}</div>
|
||||
<div>Out: {model.output_modalities?.join(', ') || 'N/A'}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@ -3,9 +3,12 @@
|
||||
* Replaces manual validation with strict type-safe schemas
|
||||
*/
|
||||
|
||||
//TODO: 从catalog导入
|
||||
|
||||
import * as z from 'zod'
|
||||
// Import schemas from catalog package
|
||||
import {
|
||||
ModelConfigSchema,
|
||||
ModelListSchema
|
||||
} from '../../src/schemas'
|
||||
|
||||
// Base parameter schemas
|
||||
const ParameterRangeSchema = z.object({
|
||||
@ -25,21 +28,6 @@ const ParameterUnsupportedSchema = z.object({
|
||||
|
||||
const ParameterValueSchema = z.union([ParameterRangeSchema, ParameterBooleanSchema, ParameterUnsupportedSchema])
|
||||
|
||||
// Model parameters schema
|
||||
const ModelParametersSchema = z
|
||||
.object({
|
||||
temperature: ParameterValueSchema.optional(),
|
||||
max_tokens: z.union([
|
||||
z.boolean(), // Simple boolean support indicator
|
||||
z.object({
|
||||
supported: z.literal(true),
|
||||
default: z.number().positive().optional()
|
||||
})
|
||||
]).optional(),
|
||||
system_message: z.boolean().optional(), // Simple boolean support indicator
|
||||
top_p: z.union([ParameterValueSchema, ParameterUnsupportedSchema]).optional()
|
||||
})
|
||||
.loose() // Allow additional parameter types
|
||||
|
||||
// Pricing schema
|
||||
const PricingInfoSchema = z.object({
|
||||
@ -53,31 +41,9 @@ const PricingInfoSchema = z.object({
|
||||
})
|
||||
})
|
||||
|
||||
// Model metadata schema
|
||||
const ModelMetadataSchema = z
|
||||
.object({
|
||||
source: z.string().optional(),
|
||||
original_provider: z.string().optional(),
|
||||
supports_caching: z.boolean().optional()
|
||||
})
|
||||
.loose() // Allow additional metadata
|
||||
|
||||
// Complete Model schema
|
||||
export const ModelSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().optional(),
|
||||
owned_by: z.string().optional(),
|
||||
capabilities: z.array(z.string()),
|
||||
input_modalities: z.array(z.string()),
|
||||
output_modalities: z.array(z.string()),
|
||||
context_window: z.number().positive(),
|
||||
max_output_tokens: z.number().positive(),
|
||||
max_input_tokens: z.number().positive().optional(),
|
||||
pricing: PricingInfoSchema.optional(),
|
||||
parameters: ModelParametersSchema.optional(),
|
||||
endpoint_types: z.array(z.string()).optional(),
|
||||
metadata: ModelMetadataSchema.optional()
|
||||
})
|
||||
// Complete Model schema - use from catalog package
|
||||
export const ModelSchema = ModelConfigSchema
|
||||
|
||||
// Provider behaviors schema
|
||||
const ProviderBehaviorsSchema = z
|
||||
@ -150,11 +116,8 @@ export const ProviderSchema = z.object({
|
||||
metadata: ProviderMetadataSchema.optional()
|
||||
})
|
||||
|
||||
// Data file schemas
|
||||
export const ModelsDataFileSchema = z.object({
|
||||
version: z.string().min(1),
|
||||
models: z.array(ModelSchema)
|
||||
})
|
||||
// Data file schemas - use from catalog package
|
||||
export const ModelsDataFileSchema = ModelListSchema
|
||||
|
||||
export const ProvidersDataFileSchema = z.object({
|
||||
version: z.string().min(1),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user