mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
refactor: extract shared buildPiModel to @openchatlab/node-runtime
Move URL normalization helpers and PiModel construction into a shared llm-builder module. Electron and server both delegate to the core builder, with Electron adding optional headers and provider validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
9c2b578c87
commit
73569c69bf
6 files changed
+272
-188
No files matched your search
@@ -13,7 +13,7 @@ import { aiLogger } from '../logger'
|
||||
import { resolveApiKey, writeAuthProfile } from '@openchatlab/config'
|
||||
import { buildChatLabUserAgentHeaders } from '../../utils/httpHeaders'
|
||||
import { t } from '../../i18n'
|
||||
import { completeSimple, type PiModel, type PiApi } from '@openchatlab/node-runtime'
|
||||
import { completeSimple, buildPiModel as buildPiModelCore, type PiModel, type PiApi } from '@openchatlab/node-runtime'
|
||||
|
||||
// 新模型系统导出
|
||||
export { BUILTIN_PROVIDERS, getBuiltinProviderById } from './provider-registry'
|
||||
@@ -424,106 +424,13 @@ export function getProviderInfo(provider: LLMProvider): ProviderInfo | null {
|
||||
|
||||
// ==================== pi-ai Model 构建 ====================
|
||||
|
||||
/**
|
||||
* 规范化 Anthropic baseUrl:Anthropic SDK 内部会拼接 /v1/messages,
|
||||
* 因此 baseUrl 不应包含 /v1 后缀,否则会导致 /v1/v1/messages 双重路径。
|
||||
*/
|
||||
function normalizeAnthropicBaseUrl(url: string): string {
|
||||
return url.replace(/\/v1\/?$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 OpenAI Compatible baseUrl:
|
||||
* 用户经常忘记在域名后加 /v1,OpenAI SDK 不会自动补全。
|
||||
* 如果 URL 没有以 /v1 结尾且路径部分为空或仅有 /,自动补上。
|
||||
* 已有具体路径(如 /api/v1、/proxy)的不做修改。
|
||||
*/
|
||||
function normalizeOpenAICompatibleBaseUrl(url: string): string {
|
||||
if (!url) return url
|
||||
const trimmed = url.replace(/\/+$/, '')
|
||||
if (trimmed.endsWith('/v1')) return trimmed
|
||||
try {
|
||||
const parsed = new URL(trimmed)
|
||||
// 仅当路径为空或 "/" 时补全 /v1,避免破坏已有的自定义路径
|
||||
if (parsed.pathname === '/' || parsed.pathname === '') {
|
||||
return trimmed + '/v1'
|
||||
}
|
||||
} catch {
|
||||
// URL 解析失败,不做处理
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW = 128000
|
||||
|
||||
export function buildPiModel(config: AIServiceConfig): PiModel<PiApi> {
|
||||
const providerDef = getBuiltinProviderById(config.provider)
|
||||
const providerInfo = getProviderInfo(config.provider)
|
||||
const baseUrl = config.baseUrl || providerDef?.defaultBaseUrl || providerInfo?.defaultBaseUrl || ''
|
||||
const modelId = config.model || providerInfo?.models?.[0]?.id || ''
|
||||
validateProviderBaseUrl(config.provider, config.baseUrl)
|
||||
|
||||
validateProviderBaseUrl(config.provider, baseUrl)
|
||||
|
||||
const modelDef = findModelDefinition(config.provider, modelId)
|
||||
const contextWindow = modelDef?.contextWindow ?? DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
const BUILTIN_PROVIDER_API: Record<string, PiApi> = {
|
||||
gemini: 'google-generative-ai',
|
||||
anthropic: 'anthropic-messages',
|
||||
}
|
||||
|
||||
const apiFormat: PiApi = (config.apiFormat as PiApi) || BUILTIN_PROVIDER_API[config.provider] || 'openai-completions'
|
||||
|
||||
if (apiFormat === 'google-generative-ai') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'google-generative-ai',
|
||||
provider: 'google',
|
||||
baseUrl,
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
if (apiFormat === 'anthropic-messages') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'anthropic-messages',
|
||||
provider: 'anthropic',
|
||||
baseUrl: normalizeAnthropicBaseUrl(baseUrl),
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
// openai-compatible + openai-completions/openai-responses:自动补全 /v1(用户经常忘记)
|
||||
const resolvedBaseUrl =
|
||||
config.provider === 'openai-compatible' && (apiFormat === 'openai-completions' || apiFormat === 'openai-responses')
|
||||
? normalizeOpenAICompatibleBaseUrl(baseUrl)
|
||||
: baseUrl
|
||||
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: apiFormat,
|
||||
provider: config.provider,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
return buildPiModelCore(config, {
|
||||
findModelFn: findModelDefinition,
|
||||
headers: config.provider === 'openai-compatible' ? buildChatLabUserAgentHeaders() : undefined,
|
||||
reasoning: config.isReasoningModel ?? false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 4096,
|
||||
compat: config.disableThinking ? { thinkingFormat: 'qwen' } : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 远程模型列表获取 ====================
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import { buildPiModel, normalizeAnthropicBaseUrl, normalizeOpenAICompatibleBaseUrl } from '../llm-builder'
|
||||
|
||||
describe('normalizeAnthropicBaseUrl', () => {
|
||||
it('strips trailing /v1', () => {
|
||||
assert.equal(normalizeAnthropicBaseUrl('https://api.anthropic.com/v1'), 'https://api.anthropic.com')
|
||||
})
|
||||
|
||||
it('strips trailing /v1/', () => {
|
||||
assert.equal(normalizeAnthropicBaseUrl('https://api.anthropic.com/v1/'), 'https://api.anthropic.com')
|
||||
})
|
||||
|
||||
it('leaves URL without /v1 unchanged', () => {
|
||||
assert.equal(normalizeAnthropicBaseUrl('https://api.anthropic.com'), 'https://api.anthropic.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeOpenAICompatibleBaseUrl', () => {
|
||||
it('appends /v1 when path is empty', () => {
|
||||
assert.equal(normalizeOpenAICompatibleBaseUrl('https://api.example.com'), 'https://api.example.com/v1')
|
||||
})
|
||||
|
||||
it('appends /v1 when path is /', () => {
|
||||
assert.equal(normalizeOpenAICompatibleBaseUrl('https://api.example.com/'), 'https://api.example.com/v1')
|
||||
})
|
||||
|
||||
it('does not append when already ends with /v1', () => {
|
||||
assert.equal(normalizeOpenAICompatibleBaseUrl('https://api.example.com/v1'), 'https://api.example.com/v1')
|
||||
})
|
||||
|
||||
it('does not modify URLs with custom paths', () => {
|
||||
assert.equal(normalizeOpenAICompatibleBaseUrl('https://api.example.com/proxy'), 'https://api.example.com/proxy')
|
||||
})
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
assert.equal(normalizeOpenAICompatibleBaseUrl(''), '')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildPiModel', () => {
|
||||
it('builds a Google Generative AI model', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'gemini',
|
||||
model: 'gemini-2.0-flash',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
})
|
||||
assert.equal(model.api, 'google-generative-ai')
|
||||
assert.equal(model.provider, 'google')
|
||||
assert.equal(model.id, 'gemini-2.0-flash')
|
||||
})
|
||||
|
||||
it('builds an Anthropic model with normalized URL', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
baseUrl: 'https://api.anthropic.com/v1',
|
||||
})
|
||||
assert.equal(model.api, 'anthropic-messages')
|
||||
assert.equal(model.baseUrl, 'https://api.anthropic.com')
|
||||
})
|
||||
|
||||
it('builds an OpenAI model with default apiFormat', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
})
|
||||
assert.equal(model.api, 'openai-completions')
|
||||
})
|
||||
|
||||
it('normalizes openai-compatible URLs', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'openai-compatible',
|
||||
model: 'custom-model',
|
||||
baseUrl: 'https://my-gateway.example.com',
|
||||
apiFormat: 'openai-completions',
|
||||
})
|
||||
assert.equal(model.baseUrl, 'https://my-gateway.example.com/v1')
|
||||
})
|
||||
|
||||
it('passes custom headers via options', () => {
|
||||
const model = buildPiModel(
|
||||
{ provider: 'openai-compatible', model: 'test', baseUrl: 'https://test.com/v1' },
|
||||
{ headers: { 'X-Custom': 'value' } }
|
||||
)
|
||||
assert.deepEqual(model.headers, { 'X-Custom': 'value' })
|
||||
})
|
||||
|
||||
it('sets compat.thinkingFormat when disableThinking is true', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'openai',
|
||||
model: 'test',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
disableThinking: true,
|
||||
})
|
||||
assert.deepEqual(model.compat, { thinkingFormat: 'qwen' })
|
||||
})
|
||||
|
||||
it('sets reasoning flag from isReasoningModel', () => {
|
||||
const model = buildPiModel({
|
||||
provider: 'openai',
|
||||
model: 'o1',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
isReasoningModel: true,
|
||||
})
|
||||
assert.equal(model.reasoning, true)
|
||||
})
|
||||
|
||||
it('uses custom findModelFn for context window', () => {
|
||||
const model = buildPiModel(
|
||||
{ provider: 'openai', model: 'custom', baseUrl: 'https://api.openai.com/v1' },
|
||||
{
|
||||
findModelFn: (_providerId, _modelId) => ({
|
||||
id: 'custom',
|
||||
providerId: 'openai',
|
||||
name: 'Custom',
|
||||
contextWindow: 32000,
|
||||
capabilities: ['chat'] as const,
|
||||
recommendedFor: ['chat'] as const,
|
||||
status: 'stable' as const,
|
||||
builtin: false,
|
||||
editable: true,
|
||||
}),
|
||||
}
|
||||
)
|
||||
assert.equal(model.contextWindow, 32000)
|
||||
})
|
||||
})
|
||||
@@ -62,6 +62,10 @@ export type { PreprocessingPipelineOptions, PreprocessingPipelineResult } from '
|
||||
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './agent'
|
||||
export { runAgentCore } from './agent'
|
||||
|
||||
// LLM Model Builder
|
||||
export { buildPiModel, normalizeAnthropicBaseUrl, normalizeOpenAICompatibleBaseUrl } from './llm-builder'
|
||||
export type { PiModelConfig, BuildPiModelOptions } from './llm-builder'
|
||||
|
||||
// Re-exports from @mariozechner/pi-agent-core
|
||||
export type { AgentTool, AgentToolResult } from '@mariozechner/pi-agent-core'
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Shared PiModel builder — translates an AIServiceConfig-like
|
||||
* object into a PiModel instance for @mariozechner/pi-ai.
|
||||
*
|
||||
* Used by both Electron and Server to eliminate duplicated
|
||||
* URL normalization, apiFormat mapping, and model construction.
|
||||
*/
|
||||
|
||||
import { BUILTIN_PROVIDERS, getBuiltinModelsByProvider, BUILTIN_MODELS, type ModelDefinition } from '@openchatlab/core'
|
||||
import type { Model as PiModel, Api as PiApi } from '@mariozechner/pi-ai'
|
||||
|
||||
export interface PiModelConfig {
|
||||
provider: string
|
||||
model?: string
|
||||
baseUrl?: string
|
||||
maxTokens?: number
|
||||
apiFormat?: string
|
||||
disableThinking?: boolean
|
||||
isReasoningModel?: boolean
|
||||
}
|
||||
|
||||
export interface BuildPiModelOptions {
|
||||
/** Override model definition lookup (e.g. to include custom models). */
|
||||
findModelFn?: (providerId: string, modelId: string) => ModelDefinition | null
|
||||
/** Extra headers injected into the PiModel (e.g. User-Agent). */
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip /v1 suffix from Anthropic baseUrl because the SDK
|
||||
* internally appends /v1/messages.
|
||||
*/
|
||||
export function normalizeAnthropicBaseUrl(url: string): string {
|
||||
return url.replace(/\/v1\/?$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-append /v1 to OpenAI-compatible URLs when the path is empty
|
||||
* (users frequently forget this).
|
||||
*/
|
||||
export function normalizeOpenAICompatibleBaseUrl(url: string): string {
|
||||
if (!url) return url
|
||||
const trimmed = url.replace(/\/+$/, '')
|
||||
if (trimmed.endsWith('/v1')) return trimmed
|
||||
try {
|
||||
const parsed = new URL(trimmed)
|
||||
if (parsed.pathname === '/' || parsed.pathname === '') {
|
||||
return trimmed + '/v1'
|
||||
}
|
||||
} catch {
|
||||
// URL parse failure — return as-is
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW = 128000
|
||||
|
||||
function defaultFindModel(providerId: string, modelId: string): ModelDefinition | null {
|
||||
const forProvider = getBuiltinModelsByProvider(providerId)
|
||||
return forProvider.find((m) => m.id === modelId) || BUILTIN_MODELS.find((m) => m.id === modelId) || null
|
||||
}
|
||||
|
||||
const BUILTIN_PROVIDER_API: Record<string, PiApi> = {
|
||||
gemini: 'google-generative-ai',
|
||||
anthropic: 'anthropic-messages',
|
||||
}
|
||||
|
||||
export function buildPiModel(config: PiModelConfig, options?: BuildPiModelOptions): PiModel<PiApi> {
|
||||
const providerDef = BUILTIN_PROVIDERS.find((p) => p.id === config.provider)
|
||||
const baseUrl = config.baseUrl || providerDef?.defaultBaseUrl || ''
|
||||
const modelId = config.model || ''
|
||||
|
||||
const findModel = options?.findModelFn ?? defaultFindModel
|
||||
const modelDef = findModel(config.provider, modelId)
|
||||
const contextWindow = modelDef?.contextWindow ?? DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
const apiFormat: PiApi = (config.apiFormat as PiApi) || BUILTIN_PROVIDER_API[config.provider] || 'openai-completions'
|
||||
|
||||
if (apiFormat === 'google-generative-ai') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'google-generative-ai',
|
||||
provider: 'google',
|
||||
baseUrl,
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
if (apiFormat === 'anthropic-messages') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'anthropic-messages',
|
||||
provider: 'anthropic',
|
||||
baseUrl: normalizeAnthropicBaseUrl(baseUrl),
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedBaseUrl =
|
||||
config.provider === 'openai-compatible' && (apiFormat === 'openai-completions' || apiFormat === 'openai-responses')
|
||||
? normalizeOpenAICompatibleBaseUrl(baseUrl)
|
||||
: baseUrl
|
||||
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: apiFormat,
|
||||
provider: config.provider,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
headers: options?.headers,
|
||||
reasoning: config.isReasoningModel ?? false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 4096,
|
||||
compat: config.disableThinking ? { thinkingFormat: 'qwen' } : undefined,
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,10 @@ export type {
|
||||
export type { AgentCoreOptions, AgentCoreEvent, AgentCoreResult, AgentTokenUsage, SimpleHistoryMessage } from './ai'
|
||||
export { runAgentCore } from './ai'
|
||||
|
||||
// LLM Model Builder
|
||||
export { buildPiModel, normalizeAnthropicBaseUrl, normalizeOpenAICompatibleBaseUrl } from './ai'
|
||||
export type { PiModelConfig, BuildPiModelOptions } from './ai'
|
||||
|
||||
// Re-exports: @mariozechner/pi-agent-core & @mariozechner/pi-ai
|
||||
export type { AgentTool, AgentToolResult } from './ai'
|
||||
export { Type, completeSimple, streamSimple } from './ai'
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import type { PiModel, PiApi } from '@openchatlab/node-runtime'
|
||||
import { BUILTIN_PROVIDERS, getBuiltinModelsByProvider, BUILTIN_MODELS } from '@openchatlab/core'
|
||||
import type { ModelDefinition } from '@openchatlab/core'
|
||||
import { buildPiModel as buildPiModelCore, type PiModelConfig } from '@openchatlab/node-runtime'
|
||||
import { resolveApiKey, writeAuthProfile } from '@openchatlab/config'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
@@ -253,93 +252,6 @@ function loadRawConfigStore(aiDataDir: string): AIConfigStore {
|
||||
|
||||
// ==================== PiModel Builder ====================
|
||||
|
||||
const DEFAULT_CONTEXT_WINDOW = 128000
|
||||
|
||||
function findModelDefinition(providerId: string, modelId: string): ModelDefinition | null {
|
||||
const builtinForProvider = getBuiltinModelsByProvider(providerId)
|
||||
return builtinForProvider.find((m) => m.id === modelId) || BUILTIN_MODELS.find((m) => m.id === modelId) || null
|
||||
}
|
||||
|
||||
function normalizeAnthropicBaseUrl(url: string): string {
|
||||
return url.replace(/\/v1\/?$/, '')
|
||||
}
|
||||
|
||||
function normalizeOpenAICompatibleBaseUrl(url: string): string {
|
||||
if (!url) return url
|
||||
const trimmed = url.replace(/\/+$/, '')
|
||||
if (trimmed.endsWith('/v1')) return trimmed
|
||||
try {
|
||||
const parsed = new URL(trimmed)
|
||||
if (parsed.pathname === '/' || parsed.pathname === '') {
|
||||
return trimmed + '/v1'
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export function buildPiModel(config: AIServiceConfig): PiModel<PiApi> {
|
||||
const providerDef = BUILTIN_PROVIDERS.find((p) => p.id === config.provider)
|
||||
const baseUrl = config.baseUrl || providerDef?.defaultBaseUrl || ''
|
||||
const modelId = config.model || ''
|
||||
|
||||
const modelDef = findModelDefinition(config.provider, modelId)
|
||||
const contextWindow = modelDef?.contextWindow ?? DEFAULT_CONTEXT_WINDOW
|
||||
|
||||
const BUILTIN_PROVIDER_API: Record<string, PiApi> = {
|
||||
gemini: 'google-generative-ai',
|
||||
anthropic: 'anthropic-messages',
|
||||
}
|
||||
|
||||
const apiFormat: PiApi = (config.apiFormat as PiApi) || BUILTIN_PROVIDER_API[config.provider] || 'openai-completions'
|
||||
|
||||
if (apiFormat === 'google-generative-ai') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'google-generative-ai',
|
||||
provider: 'google',
|
||||
baseUrl,
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
if (apiFormat === 'anthropic-messages') {
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: 'anthropic-messages',
|
||||
provider: 'anthropic',
|
||||
baseUrl: normalizeAnthropicBaseUrl(baseUrl),
|
||||
reasoning: false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedBaseUrl =
|
||||
config.provider === 'openai-compatible' && (apiFormat === 'openai-completions' || apiFormat === 'openai-responses')
|
||||
? normalizeOpenAICompatibleBaseUrl(baseUrl)
|
||||
: baseUrl
|
||||
|
||||
return {
|
||||
id: modelId,
|
||||
name: modelId,
|
||||
api: apiFormat,
|
||||
provider: config.provider,
|
||||
baseUrl: resolvedBaseUrl,
|
||||
reasoning: config.isReasoningModel ?? false,
|
||||
input: ['text'],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow,
|
||||
maxTokens: config.maxTokens ?? 4096,
|
||||
compat: config.disableThinking ? { thinkingFormat: 'qwen' } : undefined,
|
||||
}
|
||||
return buildPiModelCore(config as PiModelConfig)
|
||||
}
|
||||
Reference in new issue
Block a user