🌳
pt0/serverF/aiF/aiSourcesAI.mts
6import { type secretNameType } from '../secretsF/getPlainSecAI.mts'
9export type SourceId = 'mixroute' | 'moonshot' | 'kimicode' | 'llama' | 'openrouter'
10export type AiUse = 'smallchat' | 'oc' | 'draw'
12export const mixrouteBaseUrl = 'https://api.mixroute.ai/v1'
13export const moonshotBaseUrl = 'https://api.moonshot.ai/v1'
14export const kimicodeBaseUrl = 'https://api.kimi.com/coding/v1'
15export const llamaBaseUrl = 'http://127.0.0.1:8081/v1'
17type OcProvider = { name: string, api: string, apiKey?: string }
18type OcModel = { name: string, ctx?: number, out?: number, toolCall?: boolean, temperature?: boolean }
20export type ModelEntry = {
21 id: string
22 src: Partial<Record<SourceId, string>>
23 use: AiUse[]
24 vendor: string
25 oc?: OcModel
28export const aiModelPool: ModelEntry[] = [
29 { id: 'gpt-5', use: ['oc'], vendor: 'openai', src: { mixroute: 'gpt-5', openrouter: 'openai/gpt-5' }, oc: { name: 'GPT-5', ctx: 400000, out: 128000, toolCall: true, temperature: true } },
30 { id: 'gemini-3.5-flash', use: ['oc'], vendor: 'google', src: { mixroute: 'gemini-3.5-flash' }, oc: { name: 'Gemini 3.5 Flash', toolCall: true, temperature: true } },
31 { id: 'kimi-k2.5', use: ['oc'], vendor: 'moonshotai', src: { mixroute: 'kimi-k2.5' }, oc: { name: 'Kimi K2.5', toolCall: true, temperature: true } },
32 { id: 'kimi-k2.7-code', use: ['oc'], vendor: 'moonshotai', src: { moonshot: 'kimi-k2.7-code' }, oc: { name: 'Kimi K2.7 Code', toolCall: true, temperature: true, ctx: 262144, out: 65536 } },
33 { id: 'k3', use: ['oc'], vendor: 'moonshotai', src: { kimicode: 'k3' }, oc: { name: 'Kimi K3', toolCall: true, temperature: true, ctx: 1000000, out: 65536 } },
34 { id: 'claude-haiku-4-5', use: ['oc'], vendor: 'anthropic', src: { mixroute: 'claude-haiku-4-5', openrouter: 'anthropic/claude-haiku-4-5' }, oc: { name: 'Claude Haiku 4.5', toolCall: true, temperature: true } },
35 { id: 'gpt-4.1-nano', use: ['oc', 'smallchat'], vendor: 'openai', src: { mixroute: 'gpt-4.1-nano', openrouter: 'openai/gpt-4.1-nano' }, oc: { name: 'GPT-4.1 Nano', ctx: 1047576, out: 32768, toolCall: true, temperature: true } },
36 { id: 'gpt-5-nano', use: ['oc', 'smallchat'], vendor: 'openai', src: { mixroute: 'gpt-5-nano' }, oc: { name: 'GPT-5 Nano', toolCall: true, temperature: true } },
37 { id: 'gemini-2.5-flash', use: ['oc', 'smallchat'], vendor: 'google', src: { mixroute: 'gemini-2.5-flash', openrouter: 'google/gemini-2.5-flash' }, oc: { name: 'Gemini 2.5 Flash', ctx: 1048576, out: 65536, toolCall: true, temperature: true } },
38 { id: 'gpt-oss-120b', use: ['oc', 'smallchat'], vendor: 'openai', src: { llama: 'gpt-oss-120b' }, oc: { name: 'GPT-OSS 120B (local)', ctx: 32768, out: 8192, toolCall: true, temperature: true } },
39 { id: 'mistral-small-2603', use: ['smallchat'], vendor: 'mistralai', src: { openrouter: 'mistralai/mistral-small-2603' } },
40 { id: 'gpt-4.1', use: ['draw'], vendor: 'openai', src: { mixroute: 'gpt-4.1', openrouter: 'openai/gpt-4.1' } },
41 { id: 'gemini-2.5-flash-image', use: ['draw'], vendor: 'google', src: { mixroute: 'gemini-2.5-flash-image', openrouter: 'google/gemini-2.5-flash-image' } },
42 { id: 'deepseek-v4-flash', use: ['draw'], vendor: 'deepseek', src: { mixroute: 'deepseek-v4-flash', openrouter: 'deepseek/deepseek-v4-flash' } },
45export const resolveVendor = (modelId: string): string | null => {
46 const slash = modelId.indexOf('/')
47 if (slash > 0) return modelId.slice(0, slash)
48 const entry = aiModelPool.find(m => m.id === modelId || Object.values(m.src).some(v => v === modelId))
49 return entry?.vendor ?? null
52export type PromptOpts = { promptStr?: string, aiMessages?: any[], stream?: boolean, max_tokens?: number, modalities?: string[], temperature?: number, response_format?: unknown, [k: string]: unknown }
54type AiSource = {
55 id: SourceId
56 baseUrl: string
57 call: (nativeModel: string, promptStr: string, key: string) => Promise<string>
58 prompt?: (nativeModel: string, opts: PromptOpts, key: string) => Promise<any>
59 ocProvider?: OcProvider
62export const aiSources: AiSource[] = [
63 {
64 id: 'mixroute',
65 baseUrl: mixrouteBaseUrl,
66 call: (m, p, k) => opnRtrStrSimple({ model: m, promptStr: p, apiKey: k, baseUrl: mixrouteBaseUrl, label: 'Mixroute' }),
67 prompt: (m, o, k) => openRouterPrompt({ ...o, model: m, apiKey: k, baseUrl: mixrouteBaseUrl, trackCost: false }),
68 ocProvider: { name: 'Mixroute', api: 'openai' },
69 },
70 {
71 id: 'moonshot',
72 baseUrl: moonshotBaseUrl,
73 call: (m, p, k) => opnRtrStrSimple({ model: m, promptStr: p, apiKey: k, baseUrl: moonshotBaseUrl, label: 'Moonshot' }),
74 prompt: (m, o, k) => openRouterPrompt({ ...o, model: m, apiKey: k, baseUrl: moonshotBaseUrl, trackCost: false }),
75 ocProvider: { name: 'Moonshot', api: 'openai' },
76 },
77 {
78 id: 'kimicode',
79 baseUrl: kimicodeBaseUrl,
80 call: (m, p, k) => opnRtrStrSimple({ model: m, promptStr: p, apiKey: k, baseUrl: kimicodeBaseUrl, label: 'Kimi Code' }),
81 prompt: (m, o, k) => openRouterPrompt({ ...o, model: m, apiKey: k, baseUrl: kimicodeBaseUrl, trackCost: false }),
82 ocProvider: { name: 'Kimi Code', api: 'openai' },
83 },
84 {
85 id: 'llama',
86 baseUrl: llamaBaseUrl,
87 call: (m, p, _k) => withTimeout(opnRtrStrSimple({ model: m, promptStr: p, apiKey: 'no-key', baseUrl: llamaBaseUrl, label: 'llama' }), 30000),
88 prompt: (m, o, _k) => openRouterPrompt({ ...o, model: m, apiKey: 'no-key', baseUrl: llamaBaseUrl, trackCost: false }),
89 ocProvider: { name: 'Local llama.cpp', api: 'openai', apiKey: 'no-key' },
90 },
91 {
92 id: 'openrouter',
93 baseUrl: 'https://openrouter.ai/api/v1',
94 call: (m, p, k) => opnRtrStrSimple({ model: m, promptStr: p, apiKey: k, label: 'OpenRouter' }),
95 prompt: (m, o, k) => openRouterPrompt({ ...o, model: m, apiKey: k }),
96 },
99const tryPlainSec = (secName: secretNameType): string | null => {
100 try { return getPlainMappedSec(secName) } catch { return null }
103export const keyBySourceFor = (openrouterKey: string | null): Record<SourceId, string | null> => ({
104 mixroute: tryPlainSec(tsSecs.mixrouteApiToken),
105 moonshot: tryPlainSec(tsSecs.moonshotApiToken),
106 kimicode: tryPlainSec(tsSecs.kimicodeApiToken),
107 llama: 'no-key',
108 openrouter: openrouterKey,
109})
111export const findPoolEntry = (modelId: string): { src: Partial<Record<SourceId, string>> } =>
112 aiModelPool.find(m => m.id === modelId || m.src.openrouter === modelId) || { src: { openrouter: modelId } }
114export const smallChatCandidates = (openrouterKey: string | null): string[] => {
115 const keyBySource = keyBySourceFor(openrouterKey)
116 const candidates: string[] = []
117 for (const s of aiSources) {
118 if (keyBySource[s.id] == null) continue
119 for (const m of aiModelPool) {
120 if (!m.use.includes('smallchat')) continue
121 const native = m.src[s.id]
122 if (native) candidates.push(`${s.id}/${native}`)
123 }
124 }
125 return candidates
128export const runSmallChatWithCandidates = async (candidates: string[], promptStr: string, { context, openrouterKey, isSuccess = (r: string) => !!r?.trim() }: { context: string, openrouterKey: string | null, isSuccess?: (r: string) => boolean }) => {
129 const keyBySource = keyBySourceFor(openrouterKey)
130 let usedModel = ''
131 const { result, attempts } = await tryWithModelFallbackDetailed({
132 candidates,
133 callFn: (scoped) => {
134 usedModel = scoped
135 const slashIdx = scoped.indexOf('/')
136 const sid = scoped.slice(0, slashIdx) as SourceId
137 const native = scoped.slice(slashIdx + 1)
138 const s = aiSources.find(x => x.id === sid)
139 assertDefined(s, { scoped })
140 const key = keyBySource[sid]
141 assertDefined(key, { sid })
142 return s.call(native, promptStr, key)
143 },
144 context,
145 isSuccess,
146 })
147 return { result, attempts, usedModel }
150export const runSmallChatSources = async (promptStr: string, { context, openrouterKey }: { context: string, openrouterKey: string | null }) => {
151 const candidates = smallChatCandidates(openrouterKey)
152 return runSmallChatWithCandidates(candidates, promptStr, { context, openrouterKey })
155export const promptCandidatesFor = (modelIds: string[]): string[] => {
156 const keyBySource = keyBySourceFor(tryPlainSec(tsSecs.openrouterApiToken))
157 const candidates: string[] = []
158 for (const modelId of modelIds) {
159 const entry = findPoolEntry(modelId)
160 for (const s of aiSources) {
161 if (keyBySource[s.id] == null) continue
162 const native = entry.src[s.id]
163 if (native) candidates.push(`${s.id}/${native}`)
164 }
165 }
166 return candidates
169export const runPromptSources = async ({ modelIds, opts, context }: { modelIds: string[], opts: PromptOpts, context: string }) => {
170 const keyBySource = keyBySourceFor(tryPlainSec(tsSecs.openrouterApiToken))
171 const candidates = promptCandidatesFor(modelIds)
172 let usedModel = ''
173 const { result, attempts } = await tryWithModelFallbackDetailed({
174 candidates,
175 callFn: (scoped) => {
176 usedModel = scoped
177 const slashIdx = scoped.indexOf('/')
178 const sid = scoped.slice(0, slashIdx) as SourceId
179 const native = scoped.slice(slashIdx + 1)
180 const s = aiSources.find(x => x.id === sid)
181 assertDefined(s, { scoped })
182 const key = keyBySource[sid]
183 assertDefined(key, { sid })
184 return s.prompt!(native, opts, key)
185 },
186 context,
187 })
188 return { result, attempts, usedModel }