1import * as _ from 'lodash-es' 17export const aiAbortSig = new AbortController() 18export const aiCoderModel = 'anthropic/claude-4-opus-20250522' 22type OpnRtrApiOpts = {method: string, bodyH?: Record<string, unknown>, apiKey?: string} 24export const opnRtrApi = async (urlPath: string, {method, bodyH, apiKey}: OpnRtrApiOpts) => { 25 const body = bodyH ? JSON.stringify(bodyH) : undefined 26 const token = apiKey || getOpenRouterApiToken() 27 const headers: Record<string, string> = { 28 'Authorization': `Bearer ${token}`, 29 'Content-Type': 'application/json', 32 Object.assign(headers, { 36 const ret = await fetch('https://openrouter.ai' + urlPath, { 37 signal: aiAbortSig.signal, 46type AiMessage = {role: string, content: string | unknown[]} 48export type AiUsageEntry = {label: string, model: string, promptTokens: number, completionTokens: number, costUsd: number | null} 49export const aiUsageCtx = genContext<AiUsageEntry[]>('aiUsage') 50export const recordAiUsage = ({label, model, usage}: {label: string, model: string, usage: any}) => { 51 const store = aiUsageCtx.getStore() 52 if (!store || !usage) return 53 store.push({label, model, promptTokens: usage.prompt_tokens ?? 0, completionTokens: usage.completion_tokens ?? 0, costUsd: usage.cost ?? null}) 56export const costUsdForUsage = async ({model, usage}: {model: string, usage: {cost?: number | null, prompt_tokens: number, completion_tokens: number}}): Promise<number | null> => { 57 if (usage.cost != null) return usage.cost 59 const {pricing} = await getModelInfo({model}) 60 return usage.prompt_tokens * parseFloat(pricing.prompt) + usage.completion_tokens * parseFloat(pricing.completion) 61 } catch { return null } 64export const openRouterPrompt = async ({promptStr, aiMessages: messages, stream=false, model, apiKey, baseUrl = 'https://openrouter.ai/api/v1', trackCost = true, ...paramsH}: {promptStr?: string, aiMessages?: AiMessage[], stream?: boolean, model: string, apiKey?: string, baseUrl?: string, trackCost?: boolean, [k: string]: unknown}) => { 74 ...(trackCost ? { require_parameters: true } : {}), 79 const body = JSON.stringify(bodyH) 80 const token = apiKey || getOpenRouterApiToken() 81 const resp = await fetch(`${baseUrl}/chat/completions`, { 82 signal: aiAbortSig.signal, 85 'Authorization': `Bearer ${token}`, 86 'Content-Type': 'application/json', 94 for await (const chunk of resp.body as any) { 95 const str = new TextDecoder().decode(chunk) 96 const match = str.match(/data\: (.+)/) 99 const {content} = obj.choices[0].delta 100 totContent += content 101 process.stdout.write(content) 103 // console.log('streamed', str) 106 process.stdout.write('\n') 109 const respH: any = await resp.json() 111 const {error} = respH 113 const isModelErr = error.code === 400 || /model|invalid/i.test(error.message || '') 119 const {usage} = respH 121 const costUsd = await costUsdForUsage({model, usage}) 126 prompt_tokens: usage.prompt_tokens, 127 completion_tokens: usage.completion_tokens, 128 costCents: costUsd * 100, 131 respH.costUsd = costUsd 133 respH.costUsd = respH.usage?.cost ?? 0 139export const aiUsrMsg = (content: string) => { 141 return {role: 'user', content} 144export const aiAssMsg = (content: string) => { 146 return {role: 'assistant', content} 149export const opnRtrStr = async (props: {promptStr?: string, aiMessages?: AiMessage[], model: string, apiKey?: string, [k: string]: unknown}) => { 150 return (await openRouterPrompt(props)).choices[0].message.content 153export const opnRtrStrSimple = async ({ model, promptStr, apiKey, max_tokens = 1024, baseUrl = 'https://openrouter.ai/api/v1', label = 'OpenRouter' }: {model: string, promptStr: string, apiKey: string, max_tokens?: number, baseUrl?: string, label?: string}) => { 154 const resp = await fetch(`${baseUrl}/chat/completions`, { 157 'Authorization': `Bearer ${apiKey}`, 158 'Content-Type': 'application/json', 160 body: JSON.stringify({ 163 messages: [{ role: 'user', content: promptStr }], 167 const errBody = await resp.text().catch(() => '') 168 if (resp.status === 400 || resp.status === 404 || /model|invalid/i.test(errBody)) markModelFailed(model) 169 throw new Error(`${label} ${resp.status}: ${errBody.slice(0, 200)}`) 171 const data: any = await resp.json() 173 if (data.error.code === 400 || /model|invalid/i.test(data.error.message || '')) markModelFailed(model) 174 throw new Error(`${label}: ${data.error.message || JSON.stringify(data.error)}`) 176 recordAiUsage({label, model, usage: data.usage}) 177 return data.choices[0].message.content 180export const getAllOpenRouterModelIds = async (): Promise<string[]> => { 182 const resp = await opnRtrApi(`/api/v1/models`, {method: 'GET'}) 183 const data: any = await resp.json() 185 return _.map(data.data, 'id') as string[] 189export const getModelInfo = async ({model}: {model: string}): Promise<any> => { 191 const allModelIds = await getAllOpenRouterModelIds() 192 const resp = await opnRtrApi(`/api/v1/models`, {method: 'GET'}) 193 const data: any = await resp.json() 194 const matched = _.filter(data.data, ({id, canonical_slug}: {id: string, canonical_slug: string}) => { 195 return id == model || canonical_slug == model