botModelSettings.ts 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import { z } from 'zod'
  2. import type { BotModelSettings, ResolvedBotModelSettings } from '../shared/botModelSettings.js'
  3. import type { GatewayTarget } from './workspaceGateway.js'
  4. import { HttpError } from './errors.js'
  5. const prefix = '/api/plugins/yaoyao-bot-bridge'
  6. const id = z.string().min(1).max(512).refine(value => !/[\s"'\\\u0000-\u001f]/.test(value) && !value.startsWith('-'))
  7. const effort = z.enum(['none','minimal','low','medium','high','xhigh','max','ultra'])
  8. const speed = z.enum(['normal','fast','auto','cold'])
  9. const effective = z.object({ provider: id, model: id, reasoningEffort: effort, fastMode: speed })
  10. const resolved = z.object({ version: z.literal(1), effective, confirmationMessage: z.string().max(10000).nullable() })
  11. const catalogue = z.object({ version: z.literal(1), defaults: effective, models: z.array(z.object({
  12. provider: id, model: id, name: z.string(), reasoningEfforts: z.array(effort), reasoningKnown: z.boolean(), fastModes: z.array(speed),
  13. })) })
  14. async function request(target: GatewayTarget, profile: string, settings?: BotModelSettings | null) {
  15. const response = await target.session.request(prefix + (settings === undefined ? '/model-options' : '/model-settings/resolve'), settings === undefined
  16. ? { search: new URLSearchParams({ profile }), cache: 'reload' }
  17. : { method: 'POST', body: { profile, settings }, cache: 'reload' })
  18. let body: any
  19. try { body = JSON.parse(response.body.toString()) } catch { /* fail with the versioned boundary below */ }
  20. if (response.status === 404 || response.status === 405) throw new HttpError(409, '请升级夭夭工具桥并重启 Hermes,当前版本尚不支持 Bot 模型设置。', 'model_settings_upgrade_required')
  21. if (response.status !== 200) throw new HttpError(response.status === 400 ? 400 : 503, typeof body?.detail === 'string' ? body.detail : 'Bot 模型服务暂不可用,请重试。', 'model_settings_unavailable')
  22. return body
  23. }
  24. export async function botModelOptions(target: GatewayTarget, profile: string) {
  25. const parsed = catalogue.safeParse(await request(target, profile))
  26. if (!parsed.success) throw new HttpError(409, '工具桥模型目录格式不兼容,请升级工具桥。', 'model_settings_upgrade_required')
  27. return parsed.data
  28. }
  29. export async function resolveBotModelSettings(target: GatewayTarget, profile: string, settings: BotModelSettings | null) {
  30. const parsed = resolved.safeParse(await request(target, profile, settings))
  31. if (!parsed.success) throw new HttpError(409, '工具桥模型配置格式不兼容,请升级工具桥。', 'model_settings_upgrade_required')
  32. return parsed.data
  33. }
  34. export const botModelTarget = (value: Pick<ResolvedBotModelSettings, 'provider' | 'model'>) => JSON.stringify([value.provider, value.model])
  35. export class BotModelConfirmationError extends HttpError {
  36. constructor(readonly target: string, readonly confirmationMessage: string) {
  37. super(409, '此模型需要确认,请打开 Bot 个人资料重新保存模型设置。', 'model_confirmation_required')
  38. }
  39. }
  40. /** Always applied to an idle session; acknowledgements precede prompt submission. */
  41. export async function applyBotModelSettings(
  42. rpc: (method: string, params: Record<string, unknown>) => Promise<any>, sessionId: string,
  43. value: ResolvedBotModelSettings, confirmed: boolean,
  44. current?: { model?: string; provider?: string },
  45. ) {
  46. for (const [key, input] of [['model', `${value.model} --provider ${value.provider} --session`], ['reasoning', value.reasoningEffort], ['fast', value.fastMode]]) {
  47. if (key === 'model' && current?.model === value.model && current?.provider === value.provider) continue
  48. const result = await rpc('config.set', { session_id: sessionId, key, value: input, scope: 'session', ...(key === 'model' && confirmed ? { confirm_expensive_model: true } : {}) })
  49. if (result?.confirm_required) throw new BotModelConfirmationError(botModelTarget(value), String(result.confirm_message || result.warning || '此会话切换模型可能增加费用,请确认继续。').slice(0, 10000))
  50. if (result?.deferred || (key === 'model' && result?.scope !== 'session') || result?.value !== (key === 'model' ? value.model : input)) throw new HttpError(502, 'Hermes 未确认 Bot 会话模型设置,本轮未发送,请检查模型服务后重试。', 'model_settings_not_applied')
  51. }
  52. }