Files
page-agent/packages/llms/src/utils.ts

153 lines
4.6 KiB
TypeScript

/**
* Utility functions for LLM integration
*/
import chalk from 'chalk'
import * as z from 'zod/v4'
import type { Tool } from './types'
const debug = console.debug.bind(console, chalk.gray('[LLM]'))
/**
* Convert Zod schema to OpenAI tool format
* Uses Zod 4 native z.toJSONSchema()
*/
export function zodToOpenAITool(name: string, tool: Tool) {
return {
type: 'function' as const,
function: {
name,
description: tool.description,
parameters: z.toJSONSchema(tool.inputSchema, { target: 'openapi-3.0' }),
},
}
}
/**
* Patch model specific parameters
* @note in-place modification
*/
export function modelPatch(body: Record<string, any>) {
const model: string = body.model || ''
if (!model) return body
const modelName = normalizeModelName(model)
if (modelName.startsWith('qwen')) {
debug('Applying Qwen patch: use higher temperature for auto fixing')
body.temperature = Math.max(body.temperature || 0, 1.0)
body.enable_thinking = false
}
if (modelName.startsWith('claude')) {
debug('Applying Claude patch: disable thinking')
body.thinking = { type: 'disabled' }
// Convert tool_choice to Claude format
if (body.tool_choice === 'required') {
// 'required' -> { type: 'any' } (must call some tool)
debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }')
body.tool_choice = { type: 'any' }
} else if (body.tool_choice?.function?.name) {
// { type: 'function', function: { name: '...' } } -> { type: 'tool', name: '...' }
debug('Applying Claude patch: convert tool_choice format')
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
}
// TODO: Claude naming pattern has changed
// needs proper handling
if (modelName.startsWith('claude-opus-4-7') || modelName.startsWith('claude-opus-47')) {
debug('Applying Claude-4.7 patch: remove temperature')
delete body.temperature
}
}
if (modelName.startsWith('grok')) {
debug('Applying Grok patch: removing tool_choice')
delete body.tool_choice
debug('Applying Grok patch: disable reasoning and thinking')
body.thinking = { type: 'disabled', effort: 'minimal' }
body.reasoning = { enabled: false, effort: 'low' }
}
if (modelName.startsWith('gpt')) {
debug('Applying GPT patch: set verbosity to low')
body.verbosity = 'low'
if (modelName.startsWith('gpt-52')) {
debug('Applying GPT-52 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-51')) {
debug('Applying GPT-51 patch: disable reasoning')
body.reasoning_effort = 'none'
} else if (modelName.startsWith('gpt-54')) {
debug('Applying GPT-5.4 patch: remove reasoning_effort')
delete body.reasoning_effort
} else if (modelName.startsWith('gpt-55')) {
debug('Applying GPT-5.4 patch: remove reasoning_effort and temperature')
delete body.reasoning_effort
delete body.temperature
} else if (modelName.startsWith('gpt-5-mini')) {
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
body.reasoning_effort = 'low'
body.temperature = 1
} else if (modelName.startsWith('gpt-5')) {
debug('Applying GPT-5 patch: set reasoning effort to low')
body.reasoning_effort = 'low'
}
}
if (modelName.startsWith('gemini')) {
debug('Applying Gemini patch: set reasoning effort to minimal')
body.reasoning_effort = 'minimal'
}
if (modelName.startsWith('deepseek')) {
debug('Applying DeepSeek patch: remove tool_choice')
delete body.tool_choice
}
if (modelName.startsWith('minimax')) {
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
// MiniMax API rejects temperature = 0; clamp to a small positive value
body.temperature = Math.max(body.temperature || 0, 0.01)
if (body.temperature > 1) body.temperature = 1
// MiniMax does not support parallel_tool_calls
delete body.parallel_tool_calls
}
return body
}
/**
* check if a given model ID fits a specific model name
*
* @note
* Different model providers may use different model IDs for the same model.
* For example, openai's `gpt-5.2` may called:
*
* - `gpt-5.2-version`
* - `gpt-5_2-date`
* - `GPT-52-version-date`
* - `openai/gpt-5.2-chat`
*
* They should be treated as the same model.
* Normalize them to `gpt-52`
*/
function normalizeModelName(modelName: string): string {
let normalizedName = modelName.toLowerCase()
// remove prefix before '/'
if (normalizedName.includes('/')) {
normalizedName = normalizedName.split('/')[1]
}
// remove '_'
normalizedName = normalizedName.replace(/_/g, '')
// remove '.'
normalizedName = normalizedName.replace(/\./g, '')
return normalizedName
}