Skip to content

Commit b94e855

Browse files
committed
refactor(translation): 提取 ProviderStrategy 接口消除 AI 提供商分支
- 新建 app/lib/providers/ 策略模式(types + google + deepseek + index) - 消除 AITranslation.ts 中 4 处 if (provider === ...) 分支 - 消除 ai-test.ts 中 2 处 if (provider === ...) 分支 - mapLevelToBudget/getThinkingConfig 迁移至 providers/ 策略实现内 - 用 generateText({ system }) 替代 messages 中的 system role 修复 AI SDK warning - 新增 22 个单元测试覆盖各策略方法与工厂函数
1 parent 19e2356 commit b94e855

10 files changed

Lines changed: 353 additions & 117 deletions

File tree

app/lib/AITranslation.ts

Lines changed: 9 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,16 @@
1-
import type { DeepSeekLanguageModelOptions } from '@ai-sdk/deepseek'
2-
import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
31
import type { LanguageModel, ModelMessage } from 'ai'
42
import type { ThinkingLevel } from '~/lib/stores/appConfig'
53
import type { EnrichedTweet, Entity } from '~/types'
6-
import { createDeepSeek } from '@ai-sdk/deepseek'
7-
import { createGoogleGenerativeAI } from '@ai-sdk/google'
84
import { generateText, Output, zodSchema } from 'ai'
95
import { z } from 'zod'
106
import { models } from '~/lib/constants'
7+
import { getProviderStrategy, getThinkingConfig } from '~/lib/providers'
118
import {
129
applyAITranslations,
1310
restoreEntities,
1411
serializeForAI,
1512
} from '~/lib/react-tweet'
1613

17-
/**
18-
* 将思考程度映射为 Gemini 2.0 的 thinkingBudget (token 数)
19-
*/
20-
export function mapLevelToBudget(level: ThinkingLevel): number {
21-
switch (level) {
22-
case 'minimal': return 0
23-
case 'low': return 1024
24-
case 'medium': return 4096
25-
case 'high': return 16384
26-
case 'max': return 32768
27-
default: return 0
28-
}
29-
}
30-
31-
/**
32-
* 获取对应模型的思考配置
33-
*/
34-
export function getThinkingConfig(modelName: string, level: ThinkingLevel = 'minimal') {
35-
const modelConfig = models.find(m => m.name === modelName)
36-
const thinkingConfig: any = { includeThoughts: false }
37-
38-
if (!modelConfig)
39-
return thinkingConfig
40-
41-
if (modelConfig.provider === 'google') {
42-
if (modelConfig.thinkingType === 'level') {
43-
thinkingConfig.thinkingLevel = level
44-
}
45-
else if (modelConfig.thinkingType === 'budget') {
46-
thinkingConfig.thinkingBudget = mapLevelToBudget(level)
47-
}
48-
}
49-
else if (modelConfig.provider === 'deepseek') {
50-
// DeepSeek 映射逻辑
51-
if (level === 'minimal')
52-
return 'disabled'
53-
if (level === 'max')
54-
return 'max'
55-
return 'high'
56-
}
57-
58-
return thinkingConfig
59-
}
60-
6114
/**
6215
* 将实体 Map 转换为 AI 可读的参考文本
6316
* 目的:让 AI 知道占位符背后是什么,以便更好地理解上下文,但不需要 AI 翻译它们
@@ -278,13 +231,11 @@ ${maskedText}
278231
`
279232

280233
const baseMessages: ModelMessage[] = [
281-
{ role: 'system', content: systemPrompt },
282234
{ role: 'user', content: userContent },
283235
]
284236

285237
const modelConfig = models.find(m => m.name === modelName)
286-
const isDeepSeek = modelConfig?.provider === 'deepseek'
287-
const isGoogle = modelConfig?.provider === 'google'
238+
const strategy = modelConfig ? getProviderStrategy(modelConfig.provider) : null
288239

289240
const thinkingConfig = getThinkingConfig(modelName, thinkingLevel)
290241
const expectedNewlineCount = countNewlines(maskedText)
@@ -308,22 +259,13 @@ ${maskedText}
308259
for (let attempt = 0; attempt < 2; attempt++) {
309260
const response = await generateText({
310261
model,
262+
system: systemPrompt,
311263
messages,
312264
output,
313265
temperature: 0.5,
314-
providerOptions: {
315-
...(isGoogle ? {
316-
google: {
317-
thinkingConfig,
318-
} satisfies GoogleGenerativeAIProviderOptions,
319-
} : {}),
320-
...(isDeepSeek && modelConfig?.thinkingType === 'level' ? {
321-
deepseek: {
322-
thinking: { type: thinkingConfig === 'disabled' ? 'disabled' : 'enabled' },
323-
...(thinkingConfig !== 'disabled' && typeof thinkingConfig === 'string' ? { reasoningEffort: thinkingConfig } : {}),
324-
} satisfies DeepSeekLanguageModelOptions,
325-
} : {}),
326-
},
266+
providerOptions: strategy && modelConfig
267+
? strategy.buildProviderOptions(thinkingConfig, modelConfig)
268+
: {},
327269
})
328270

329271
const translated = normalizeNewlineEscapes(response.output.translation, expectedNewlineCount).trim()
@@ -392,25 +334,16 @@ export async function autoTranslateTweet({
392334
}
393335
const entityContext = generateEntityContext(entityMap)
394336

395-
let aiProvider: any
396-
if (provider === 'google') {
397-
aiProvider = createGoogleGenerativeAI({
398-
apiKey,
399-
})
400-
}
401-
else {
402-
aiProvider = createDeepSeek({
403-
apiKey,
404-
})
405-
}
337+
const strategy = getProviderStrategy(provider)
338+
const sdkProvider = strategy.createSDKProvider(apiKey)
406339

407340
const { translatedText, entityText } = await translateText({
408341
tweet,
409342
maskedText,
410343
entityContext,
411344
placeholders: Array.from(entityMap.keys()),
412345
entityMap,
413-
model: aiProvider(model),
346+
model: sdkProvider(model),
414347
modelName: model,
415348
translationGlossary,
416349
thinkingLevel,

app/lib/providers/deepseek.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { DeepSeekLanguageModelOptions } from '@ai-sdk/deepseek'
2+
import type { ProviderStrategy } from './types'
3+
import type { ThinkingLevel } from '~/lib/stores/appConfig'
4+
import { createDeepSeek } from '@ai-sdk/deepseek'
5+
6+
function resolveReasoningEffort(level: ThinkingLevel): string {
7+
if (level === 'minimal')
8+
return 'disabled'
9+
if (level === 'max')
10+
return 'max'
11+
return 'high'
12+
}
13+
14+
export const deepseekStrategy: ProviderStrategy = {
15+
name: 'deepseek',
16+
17+
createSDKProvider(apiKey) {
18+
return createDeepSeek({ apiKey })
19+
},
20+
21+
getThinkingConfig(_modelConfig, level) {
22+
return resolveReasoningEffort(level)
23+
},
24+
25+
buildProviderOptions(thinkingConfig, modelConfig) {
26+
if (modelConfig.thinkingType !== 'level')
27+
return {}
28+
29+
return {
30+
deepseek: {
31+
thinking: {
32+
type: thinkingConfig === 'disabled' ? 'disabled' : 'enabled',
33+
},
34+
...(thinkingConfig !== 'disabled' && typeof thinkingConfig === 'string'
35+
? { reasoningEffort: thinkingConfig }
36+
: {}),
37+
},
38+
} satisfies Record<string, DeepSeekLanguageModelOptions>
39+
},
40+
}

app/lib/providers/google.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
2+
import type { ProviderStrategy } from './types'
3+
import type { ThinkingLevel } from '~/lib/stores/appConfig'
4+
import { createGoogleGenerativeAI } from '@ai-sdk/google'
5+
6+
function mapLevelToBudget(level: ThinkingLevel): number {
7+
switch (level) {
8+
case 'minimal': return 0
9+
case 'low': return 1024
10+
case 'medium': return 4096
11+
case 'high': return 16384
12+
case 'max': return 32768
13+
default: return 0
14+
}
15+
}
16+
17+
export const googleStrategy: ProviderStrategy = {
18+
name: 'google',
19+
20+
createSDKProvider(apiKey) {
21+
return createGoogleGenerativeAI({ apiKey })
22+
},
23+
24+
getThinkingConfig(modelConfig, level) {
25+
const config: any = { includeThoughts: false }
26+
if (modelConfig.thinkingType === 'level') {
27+
config.thinkingLevel = level
28+
}
29+
else if (modelConfig.thinkingType === 'budget') {
30+
config.thinkingBudget = mapLevelToBudget(level)
31+
}
32+
return config
33+
},
34+
35+
buildProviderOptions(thinkingConfig, _modelConfig) {
36+
return {
37+
google: { thinkingConfig },
38+
} satisfies Record<string, GoogleGenerativeAIProviderOptions>
39+
},
40+
}

app/lib/providers/index.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { ProviderStrategy } from './types'
2+
import type { ThinkingLevel } from '~/lib/stores/appConfig'
3+
import { models } from '~/lib/constants'
4+
import { deepseekStrategy } from './deepseek'
5+
import { googleStrategy } from './google'
6+
7+
const strategies: Record<string, ProviderStrategy> = {
8+
google: googleStrategy,
9+
deepseek: deepseekStrategy,
10+
}
11+
12+
/**
13+
* Get the ProviderStrategy for a given provider name.
14+
* Throws if the provider is unknown.
15+
*/
16+
export function getProviderStrategy(provider: string): ProviderStrategy {
17+
const strategy = strategies[provider]
18+
if (!strategy) {
19+
throw new Error(`Unknown AI provider: ${provider}. Supported: ${Object.keys(strategies).join(', ')}`)
20+
}
21+
return strategy
22+
}
23+
24+
/**
25+
* Convenience: resolve model name → model config → strategy → thinking config.
26+
* Returns `{ includeThoughts: false }` as fallback for unknown models.
27+
*/
28+
export function getThinkingConfig(modelName: string, level: ThinkingLevel = 'minimal') {
29+
const modelConfig = models.find(m => m.name === modelName)
30+
if (!modelConfig) {
31+
return { includeThoughts: false }
32+
}
33+
const strategy = getProviderStrategy(modelConfig.provider)
34+
return strategy.getThinkingConfig(modelConfig, level)
35+
}
36+
37+
export { deepseekStrategy, googleStrategy }
38+
export type { ProviderStrategy }

app/lib/providers/types.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ModelConfig } from '~/lib/constants'
2+
import type { ThinkingLevel } from '~/lib/stores/appConfig'
3+
4+
export interface ProviderStrategy {
5+
readonly name: string
6+
7+
/**
8+
* Create an AI SDK provider instance from an API key.
9+
* Returns a callable that accepts a model name and returns a LanguageModel.
10+
*/
11+
createSDKProvider: (apiKey: string) => any
12+
13+
/**
14+
* Build thinking configuration for generateText.
15+
* Maps a user-selected ThinkingLevel to provider-specific config.
16+
*/
17+
getThinkingConfig: (modelConfig: ModelConfig, level: ThinkingLevel) => any
18+
19+
/**
20+
* Build the providerOptions object for generateText.
21+
* Wraps thinking config into the provider-specific options structure.
22+
*/
23+
buildProviderOptions: (thinkingConfig: any, modelConfig: ModelConfig) => Record<string, any>
24+
}

app/routes/api/ai/ai-test.ts

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import type { DeepSeekLanguageModelOptions } from '@ai-sdk/deepseek'
2-
import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google'
31
import type { ModelMessage } from 'ai'
42
import type { Route } from './+types/ai-test'
5-
import { createDeepSeek } from '@ai-sdk/deepseek'
6-
import { createGoogleGenerativeAI } from '@ai-sdk/google'
73
import { generateText } from 'ai'
84
import { data } from 'react-router'
95
import z from 'zod'
10-
import { getThinkingConfig } from '~/lib/AITranslation'
116
import { models } from '~/lib/constants'
7+
import { getProviderStrategy, getThinkingConfig } from '~/lib/providers'
128
import { getTweetSchema } from '~/lib/validations/tweet'
139

1410
export async function action({ request }: Route.ActionArgs) {
@@ -44,17 +40,8 @@ export async function action({ request }: Route.ActionArgs) {
4440
const provider = modelConfig?.provider || 'google'
4541

4642
try {
47-
let aiProvider: any
48-
if (provider === 'google') {
49-
aiProvider = createGoogleGenerativeAI({
50-
apiKey,
51-
})
52-
}
53-
else {
54-
aiProvider = createDeepSeek({
55-
apiKey,
56-
})
57-
}
43+
const strategy = getProviderStrategy(provider)
44+
const aiProvider = strategy.createSDKProvider(apiKey)
5845

5946
const messages: ModelMessage[] = [
6047
{ role: 'user', content: 'hello' },
@@ -66,18 +53,9 @@ export async function action({ request }: Route.ActionArgs) {
6653
model: aiProvider(model),
6754
messages,
6855
temperature: 1,
69-
providerOptions: {
70-
...(provider === 'google' ? {
71-
google: {
72-
thinkingConfig,
73-
} satisfies GoogleGenerativeAIProviderOptions,
74-
} : {}),
75-
...(provider === 'deepseek' && model === 'deepseek-reasoner' ? {
76-
deepseek: {
77-
thinking: { type: 'enabled' },
78-
} satisfies DeepSeekLanguageModelOptions,
79-
} : {}),
80-
},
56+
providerOptions: modelConfig
57+
? strategy.buildProviderOptions(thinkingConfig, modelConfig)
58+
: {},
8159
})
8260

8361
const text = response.text.trim()

bun.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)