Skip to content

Commit e5c2f21

Browse files
authored
Merge pull request #673 from miurla/feat/json-model-config
feat: migrate model configuration to JSON files
2 parents 01cc85a + 7999c29 commit e5c2f21

File tree

5 files changed

+198
-38
lines changed

5 files changed

+198
-38
lines changed

config/models/cloud.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"version": 1,
3+
"models": {
4+
"types": {
5+
"speed": {
6+
"id": "gpt-5-mini",
7+
"name": "GPT-5 mini",
8+
"provider": "OpenAI",
9+
"providerId": "openai",
10+
"providerOptions": {
11+
"openai": {
12+
"reasoningEffort": "low",
13+
"reasoningSummary": "auto"
14+
}
15+
}
16+
},
17+
"quality": {
18+
"id": "anthropic/claude-sonnet-4",
19+
"name": "Claude Sonnet 4",
20+
"provider": "Vercel AI Gateway",
21+
"providerId": "gateway",
22+
"providerOptions": {
23+
"gateway": {
24+
"order": ["anthropic", "bedrock", "vertex"]
25+
}
26+
}
27+
}
28+
},
29+
"relatedQuestions": {
30+
"id": "gemini-2.0-flash",
31+
"name": "Gemini 2.0 Flash",
32+
"provider": "Google",
33+
"providerId": "google"
34+
}
35+
}
36+
}

config/models/default.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"version": 1,
3+
"models": {
4+
"types": {
5+
"speed": {
6+
"id": "gpt-5-mini",
7+
"name": "GPT-5 mini",
8+
"provider": "OpenAI",
9+
"providerId": "openai",
10+
"providerOptions": {
11+
"openai": {
12+
"reasoningEffort": "low",
13+
"reasoningSummary": "auto"
14+
}
15+
}
16+
},
17+
"quality": {
18+
"id": "gpt-5",
19+
"name": "GPT-5",
20+
"provider": "OpenAI",
21+
"providerId": "openai",
22+
"providerOptions": {
23+
"openai": {
24+
"reasoningEffort": "low",
25+
"reasoningSummary": "auto"
26+
}
27+
}
28+
}
29+
},
30+
"relatedQuestions": {
31+
"id": "gpt-4o-mini",
32+
"name": "GPT-4o mini",
33+
"provider": "OpenAI",
34+
"providerId": "openai"
35+
}
36+
}
37+
}

lib/agents/generate-related-questions.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { generateObject, type ModelMessage } from 'ai'
22
import { z } from 'zod'
33

4-
import { RELATED_QUESTIONS_MODEL_CONFIG } from '../config/model-types'
4+
import { getRelatedQuestionsModel } from '../config/model-types'
55
import { getModel } from '../utils/registry'
66
import { isTracingEnabled } from '../utils/telemetry'
77

@@ -22,8 +22,9 @@ export async function generateRelatedQuestions(
2222
abortSignal?: AbortSignal,
2323
parentTraceId?: string
2424
) {
25-
// Use the related questions model configuration
26-
const modelId = `${RELATED_QUESTIONS_MODEL_CONFIG.providerId}:${RELATED_QUESTIONS_MODEL_CONFIG.id}`
25+
// Use the related questions model configuration from JSON
26+
const relatedModel = getRelatedQuestionsModel()
27+
const modelId = `${relatedModel.providerId}:${relatedModel.id}`
2728

2829
const { object } = await generateObject({
2930
model: getModel(modelId),

lib/config/load-models-config.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import fsSync from 'fs'
2+
import fs from 'fs/promises'
3+
import path from 'path'
4+
5+
import { ModelType } from '@/lib/types/model-type'
6+
import { Model } from '@/lib/types/models'
7+
8+
export interface ModelsConfig {
9+
version: number
10+
models: {
11+
types: Record<ModelType, Model>
12+
relatedQuestions: Model
13+
}
14+
}
15+
16+
let cachedConfig: ModelsConfig | null = null
17+
let cachedProfile: string | null = null
18+
19+
function resolveConfigPath(): string {
20+
const profile = process.env.MORPHIC_MODELS_PROFILE?.trim() || 'default'
21+
const file = `${profile}.json`
22+
const configPath = path.resolve(process.cwd(), 'config', 'models', file)
23+
return configPath
24+
}
25+
26+
export async function loadModelsConfig(): Promise<ModelsConfig> {
27+
const profile = process.env.MORPHIC_MODELS_PROFILE?.trim() || 'default'
28+
29+
if (cachedConfig && cachedProfile === profile) {
30+
return cachedConfig
31+
}
32+
33+
const filePath = resolveConfigPath()
34+
try {
35+
const raw = await fs.readFile(filePath, 'utf-8')
36+
const json = JSON.parse(raw)
37+
38+
// Minimal validation
39+
if (!json || typeof json !== 'object') {
40+
throw new Error('Invalid models config: not an object')
41+
}
42+
if (typeof json.version !== 'number') {
43+
throw new Error('Invalid models config: missing version')
44+
}
45+
if (!json.models || typeof json.models !== 'object') {
46+
throw new Error('Invalid models config: missing models')
47+
}
48+
if (!json.models.types || !json.models.relatedQuestions) {
49+
throw new Error('Invalid models config: missing required sections')
50+
}
51+
52+
cachedConfig = json as ModelsConfig
53+
cachedProfile = profile
54+
return cachedConfig
55+
} catch (err) {
56+
// If selected profile fails, try default as a safe fallback
57+
if (profile !== 'default') {
58+
const fallbackPath = path.resolve(
59+
process.cwd(),
60+
'config',
61+
'models',
62+
'default.json'
63+
)
64+
const raw = await fs.readFile(fallbackPath, 'utf-8')
65+
const json = JSON.parse(raw)
66+
cachedConfig = json as ModelsConfig
67+
cachedProfile = 'default'
68+
return cachedConfig
69+
}
70+
throw err
71+
}
72+
}
73+
74+
// Synchronous load (for code paths that need sync access)
75+
export function loadModelsConfigSync(): ModelsConfig {
76+
const profile = process.env.MORPHIC_MODELS_PROFILE?.trim() || 'default'
77+
if (cachedConfig && cachedProfile === profile) {
78+
return cachedConfig
79+
}
80+
81+
const filePath = resolveConfigPath()
82+
try {
83+
const raw = fsSync.readFileSync(filePath, 'utf-8')
84+
const json = JSON.parse(raw)
85+
cachedConfig = json as ModelsConfig
86+
cachedProfile = profile
87+
return cachedConfig
88+
} catch (err) {
89+
if (profile !== 'default') {
90+
const fallbackPath = path.resolve(
91+
process.cwd(),
92+
'config',
93+
'models',
94+
'default.json'
95+
)
96+
const raw = fsSync.readFileSync(fallbackPath, 'utf-8')
97+
const json = JSON.parse(raw)
98+
cachedConfig = json as ModelsConfig
99+
cachedProfile = 'default'
100+
return cachedConfig
101+
}
102+
throw err
103+
}
104+
}
105+
106+
// Public accessor that ensures a config is available synchronously
107+
export function getModelsConfig(): ModelsConfig {
108+
if (!cachedConfig) {
109+
return loadModelsConfigSync()
110+
}
111+
return cachedConfig
112+
}

lib/config/model-types.ts

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,16 @@
11
import { ModelType } from '@/lib/types/model-type'
22
import { Model } from '@/lib/types/models'
33

4-
// Model configurations for each model type
5-
export const MODEL_TYPE_CONFIGS: Record<ModelType, Model> = {
6-
speed: {
7-
id: 'gpt-5-mini',
8-
name: 'GPT-5 mini',
9-
provider: 'OpenAI',
10-
providerId: 'openai',
11-
providerOptions: {
12-
openai: {
13-
reasoningEffort: 'low',
14-
reasoningSummary: 'auto'
15-
}
16-
}
17-
},
18-
quality: {
19-
id: 'anthropic/claude-sonnet-4',
20-
name: 'Claude Sonnet 4',
21-
provider: 'Vercel AI Gateway',
22-
providerId: 'gateway',
23-
providerOptions: {
24-
gateway: {
25-
order: ['anthropic', 'bedrock', 'vertext']
26-
}
27-
}
28-
}
29-
}
4+
import { getModelsConfig } from './load-models-config'
305

31-
// Model configuration for related questions generation
32-
export const RELATED_QUESTIONS_MODEL_CONFIG: Model = {
33-
id: 'gemini-2.0-flash',
34-
name: 'Gemini 2.0 Flash',
35-
provider: 'Google',
36-
providerId: 'google'
6+
// Get model for a specific type from JSON config
7+
export function getModelForType(type: ModelType): Model {
8+
const cfg = getModelsConfig()
9+
return cfg.models.types[type]
3710
}
3811

39-
// Helper function to get model for a specific type
40-
export function getModelForType(type: ModelType): Model {
41-
return MODEL_TYPE_CONFIGS[type]
12+
// Get model for related questions generation from JSON config
13+
export function getRelatedQuestionsModel(): Model {
14+
const cfg = getModelsConfig()
15+
return cfg.models.relatedQuestions
4216
}

0 commit comments

Comments
 (0)