From 8bd103bb3d179625b74604aa335424aeca8fb972 Mon Sep 17 00:00:00 2001 From: audrzejq <31422031+audrzejq@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:01:20 +0100 Subject: [PATCH] reorganize monitor package --- api/cron/monitor-chutes.ts | 2 +- api/cron/monitor-openrouter.ts | 2 +- bun.lock | 9 + models-monitor/lib.ts | 239 ------------------ package.json | 4 +- packages/monitor/index.ts | 3 + packages/monitor/package.json | 11 + .../monitor/providers}/chutes.ts | 2 +- .../monitor/providers}/openrouter.ts | 2 +- packages/monitor/src/helpers.ts | 26 ++ packages/monitor/src/runner.ts | 63 +++++ packages/monitor/src/s3.ts | 24 ++ packages/monitor/src/slack.ts | 57 +++++ packages/monitor/src/types.ts | 25 ++ packages/web/api/cron/monitor-chutes.ts | 34 --- packages/web/api/cron/monitor-openrouter.ts | 31 --- 16 files changed, 224 insertions(+), 310 deletions(-) delete mode 100644 models-monitor/lib.ts create mode 100644 packages/monitor/index.ts create mode 100644 packages/monitor/package.json rename {models-monitor => packages/monitor/providers}/chutes.ts (94%) rename {models-monitor => packages/monitor/providers}/openrouter.ts (93%) create mode 100644 packages/monitor/src/helpers.ts create mode 100644 packages/monitor/src/runner.ts create mode 100644 packages/monitor/src/s3.ts create mode 100644 packages/monitor/src/slack.ts create mode 100644 packages/monitor/src/types.ts delete mode 100644 packages/web/api/cron/monitor-chutes.ts delete mode 100644 packages/web/api/cron/monitor-openrouter.ts diff --git a/api/cron/monitor-chutes.ts b/api/cron/monitor-chutes.ts index c2f20dacd..e000da6b4 100644 --- a/api/cron/monitor-chutes.ts +++ b/api/cron/monitor-chutes.ts @@ -1,4 +1,4 @@ -import { runMonitor, round6 } from '../../models-monitor/lib.ts' +import { runMonitor, round6 } from '@models.dev/monitor' export const config = { maxDuration: 60 } diff --git a/api/cron/monitor-openrouter.ts b/api/cron/monitor-openrouter.ts index 5a96ebd05..9cb370994 100644 --- a/api/cron/monitor-openrouter.ts +++ b/api/cron/monitor-openrouter.ts @@ -1,4 +1,4 @@ -import { runMonitor, round6 } from '../../models-monitor/lib.ts' +import { runMonitor, round6 } from '@models.dev/monitor' export const config = { maxDuration: 60 } diff --git a/bun.lock b/bun.lock index 4c9a28f53..79f7062d7 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,13 @@ "@types/node": "catalog:", }, }, + "packages/monitor": { + "name": "@models.dev/monitor", + "version": "0.0.0", + "dependencies": { + "@aws-sdk/client-s3": "*", + }, + }, "packages/web": { "name": "@models.dev/web", "dependencies": { @@ -120,6 +127,8 @@ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + "@models.dev/monitor": ["@models.dev/monitor@workspace:packages/monitor"], + "@models.dev/web": ["@models.dev/web@workspace:packages/web"], "@smithy/abort-controller": ["@smithy/abort-controller@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g=="], diff --git a/models-monitor/lib.ts b/models-monitor/lib.ts deleted file mode 100644 index 99f5e3db5..000000000 --- a/models-monitor/lib.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3' - -// ── Types ───────────────────────────────────────────────────────────────────── - -export interface ModelEntry { - id: string - price_prompt: number // per 1M input tokens, USD - price_completion: number // per 1M output tokens, USD -} - -export interface Snapshot { - timestamp: string - models: ModelEntry[] -} - -type SlackBlock = Record - -// ── Helpers ─────────────────────────────────────────────────────────────────── - -/** Round to 6 decimal places to avoid float noise in comparisons */ -export function round6(n: number): number { - return Math.round(n * 1_000_000) / 1_000_000 -} - -function formatPrice(p: number): string { - if (p === 0) return '$0' - return `$${p.toFixed(2)}` -} - -function priceArrow(oldVal: number, newVal: number): string { - if (newVal > oldVal) return '↑' - if (newVal < oldVal) return '↓' - return '→' -} - -function pctChange(oldVal: number, newVal: number): string { - if (oldVal === 0) return '' - const pct = ((newVal - oldVal) / oldVal) * 100 - const sign = pct > 0 ? '+' : '' - return ` _(${sign}${Math.round(pct)}%)_` -} - -function truncateList(lines: string[], maxItems = 20, sep = '\n'): string { - if (lines.length <= maxItems) return lines.join(sep) - return lines.slice(0, maxItems).join(sep) + `${sep}_…and ${lines.length - maxItems} more_` -} - -// ── S3 ──────────────────────────────────────────────────────────────────────── - -const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'eu-west-1' }) - -export async function loadSnapshot(bucket: string, key: string): Promise { - try { - const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })) - const body = await res.Body!.transformToString() - return JSON.parse(body) as Snapshot - } catch (e: any) { - if (e.name === 'NoSuchKey' || e.Code === 'NoSuchKey') return null - throw e - } -} - -export async function saveSnapshot(bucket: string, key: string, models: ModelEntry[]): Promise { - const snapshot: Snapshot = { timestamp: new Date().toISOString(), models } - await s3.send( - new PutObjectCommand({ - Bucket: bucket, - Key: key, - Body: JSON.stringify(snapshot, null, 2), - ContentType: 'application/json', - }), - ) -} - -// ── Slack ───────────────────────────────────────────────────────────────────── - -export async function sendSlack(fallbackText: string, blocks: SlackBlock[]): Promise { - const webhookUrl = process.env.SLACK_WEBHOOK_URL ?? '' - if (!webhookUrl) { - console.log('[Slack] No SLACK_WEBHOOK_URL set — skipping notification') - console.log('[Slack] Message would be:', fallbackText) - return - } - - const res = await fetch(webhookUrl, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: fallbackText, blocks }), - }) - - if (!res.ok) { - console.error(`[Slack] Webhook error: ${res.status} ${await res.text()}`) - } -} - -type PriceChange = { - id: string - old_prompt: number - new_prompt: number - old_completion: number - new_completion: number -} - -export function buildSlackBlocks( - title: string, - added: ModelEntry[], - removed: ModelEntry[], - priceChanges: PriceChange[], - previousTimestamp: string, -): SlackBlock[] { - const blocks: SlackBlock[] = [ - { type: 'section', text: { type: 'mrkdwn', text: `*${title}*` } }, - { type: 'divider' }, - ] - - if (added.length > 0) { - const lines = added.map( - m => `• \`${m.id}\`\n *in:* ${formatPrice(m.price_prompt)}/1M *out:* ${formatPrice(m.price_completion)}/1M`, - ) - blocks.push({ - type: 'section', - text: { type: 'mrkdwn', text: `✅ *New models (${added.length})*\n\n${truncateList(lines, 20, '\n\n')}` }, - }) - } - - if (removed.length > 0) { - const lines = removed.map(m => `• \`${m.id}\``) - blocks.push({ - type: 'section', - text: { type: 'mrkdwn', text: `❌ *Removed models (${removed.length})*\n\n${truncateList(lines, 20, '\n\n')}` }, - }) - } - - if (priceChanges.length > 0) { - const lines = priceChanges.map(c => { - const parts: string[] = [] - if (c.old_prompt !== c.new_prompt) { - parts.push(`*in:* ${formatPrice(c.old_prompt)} ${priceArrow(c.old_prompt, c.new_prompt)} ${formatPrice(c.new_prompt)}/1M${pctChange(c.old_prompt, c.new_prompt)}`) - } - if (c.old_completion !== c.new_completion) { - parts.push(`*out:* ${formatPrice(c.old_completion)} ${priceArrow(c.old_completion, c.new_completion)} ${formatPrice(c.new_completion)}/1M${pctChange(c.old_completion, c.new_completion)}`) - } - return `• \`${c.id}\`\n ${parts.join(' ')}` - }) - blocks.push({ - type: 'section', - text: { type: 'mrkdwn', text: `💰 *Price changes (${priceChanges.length})*\n\n${truncateList(lines, 20, '\n\n')}` }, - }) - } - - blocks.push({ - type: 'context', - elements: [{ type: 'mrkdwn', text: `Checked at ${new Date().toISOString()} | Previous snapshot: ${previousTimestamp}` }], - }) - - return blocks -} - -// ── Monitor runner ──────────────────────────────────────────────────────────── - -export interface MonitorOptions { - name: string // e.g. "OpenRouter" - title: string // Slack header, e.g. "🛰 OpenRouter Models Update" - s3Key: string // e.g. "openrouter/snapshot.json" - fetchModels: () => Promise -} - -export async function runMonitor({ name, title, s3Key, fetchModels }: MonitorOptions): Promise { - const bucket = process.env.S3_BUCKET ?? 'models-monitor' - - console.log(`Fetching ${name} models...`) - const current = await fetchModels() - console.log(` ${current.length} models fetched`) - - console.log(`Loading snapshot from s3://${bucket}/${s3Key}...`) - const previous = await loadSnapshot(bucket, s3Key) - - if (!previous) { - console.log('No previous snapshot found — saving initial snapshot.') - await saveSnapshot(bucket, s3Key, current) - await sendSlack( - `${name} Monitor initialized — tracking ${current.length} models.`, - [{ - type: 'section', - text: { - type: 'mrkdwn', - text: `*🛰 ${name} Monitor initialized*\nTracking *${current.length} models*. Future runs will report changes.\n\`s3://${bucket}/${s3Key}\``, - }, - }], - ) - console.log('Done.') - return - } - - console.log(` Previous snapshot: ${previous.models.length} models (${previous.timestamp})`) - - const prevMap = new Map(previous.models.map(m => [m.id, m])) - - const added = current.filter(m => !prevMap.has(m.id)) - const removed = previous.models.filter(m => !new Map(current.map(m => [m.id, m])).has(m.id)) - - const priceChanges = current - .filter(m => { - const prev = prevMap.get(m.id) - if (!prev) return false - return prev.price_prompt !== m.price_prompt || prev.price_completion !== m.price_completion - }) - .map(m => { - const prev = prevMap.get(m.id)! - return { - id: m.id, - old_prompt: prev.price_prompt, - new_prompt: m.price_prompt, - old_completion: prev.price_completion, - new_completion: m.price_completion, - } - }) - - console.log(` Added: ${added.length} | Removed: ${removed.length} | Price changes: ${priceChanges.length}`) - - if (added.length === 0 && removed.length === 0 && priceChanges.length === 0) { - console.log('No changes detected.') - await saveSnapshot(bucket, s3Key, current) - return - } - - const summaryParts = [ - added.length > 0 ? `${added.length} new` : null, - removed.length > 0 ? `${removed.length} removed` : null, - priceChanges.length > 0 ? `${priceChanges.length} price change(s)` : null, - ].filter(Boolean) - - await sendSlack( - `${name} update: ${summaryParts.join(', ')}`, - buildSlackBlocks(title, added, removed, priceChanges, previous.timestamp), - ) - await saveSnapshot(bucket, s3Key, current) - console.log('Snapshot updated and Slack notification sent.') -} diff --git a/package.json b/package.json index e75528cc2..7bab27fda 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "helicone:generate": "bun ./packages/core/script/generate-helicone.ts", "venice:generate": "bun ./packages/core/script/generate-venice.ts", "vercel:generate": "bun ./packages/core/script/generate-vercel.ts", - "monitor:openrouter": "bun ./models-monitor/openrouter.ts", - "monitor:chutes": "bun ./models-monitor/chutes.ts" + "monitor:openrouter": "bun ./packages/monitor/providers/openrouter.ts", + "monitor:chutes": "bun ./packages/monitor/providers/chutes.ts" }, "type": "module", "workspaces": { diff --git a/packages/monitor/index.ts b/packages/monitor/index.ts new file mode 100644 index 000000000..d7c21bd00 --- /dev/null +++ b/packages/monitor/index.ts @@ -0,0 +1,3 @@ +export { runMonitor } from './src/runner.ts' +export { round6 } from './src/helpers.ts' +export type { ModelEntry, MonitorOptions } from './src/types.ts' diff --git a/packages/monitor/package.json b/packages/monitor/package.json new file mode 100644 index 000000000..91dd57dc1 --- /dev/null +++ b/packages/monitor/package.json @@ -0,0 +1,11 @@ +{ + "name": "@models.dev/monitor", + "version": "0.0.0", + "type": "module", + "main": "./index.ts", + "exports": { ".": "./index.ts" }, + "private": true, + "dependencies": { + "@aws-sdk/client-s3": "*" + } +} diff --git a/models-monitor/chutes.ts b/packages/monitor/providers/chutes.ts similarity index 94% rename from models-monitor/chutes.ts rename to packages/monitor/providers/chutes.ts index 31028aef3..3671e8972 100644 --- a/models-monitor/chutes.ts +++ b/packages/monitor/providers/chutes.ts @@ -5,7 +5,7 @@ * Usage: bun run monitor:chutes */ -import { runMonitor, round6 } from './lib.ts' +import { runMonitor, round6 } from '@models.dev/monitor' runMonitor({ name: 'Chutes', diff --git a/models-monitor/openrouter.ts b/packages/monitor/providers/openrouter.ts similarity index 93% rename from models-monitor/openrouter.ts rename to packages/monitor/providers/openrouter.ts index 7ad25a7c4..3cbefa6e5 100644 --- a/models-monitor/openrouter.ts +++ b/packages/monitor/providers/openrouter.ts @@ -4,7 +4,7 @@ * Usage: bun run monitor:openrouter */ -import { runMonitor, round6 } from './lib.ts' +import { runMonitor, round6 } from '@models.dev/monitor' runMonitor({ name: 'OpenRouter', diff --git a/packages/monitor/src/helpers.ts b/packages/monitor/src/helpers.ts new file mode 100644 index 000000000..cf7000ad1 --- /dev/null +++ b/packages/monitor/src/helpers.ts @@ -0,0 +1,26 @@ +export function round6(n: number): number { + return Math.round(n * 1_000_000) / 1_000_000 +} + +export function formatPrice(p: number): string { + if (p === 0) return '$0' + return `$${p.toFixed(2)}` +} + +export function priceArrow(oldVal: number, newVal: number): string { + if (newVal > oldVal) return '↑' + if (newVal < oldVal) return '↓' + return '→' +} + +export function pctChange(oldVal: number, newVal: number): string { + if (oldVal === 0) return '' + const pct = ((newVal - oldVal) / oldVal) * 100 + const sign = pct > 0 ? '+' : '' + return ` _(${sign}${Math.round(pct)}%)_` +} + +export function truncateList(lines: string[], maxItems = 20, sep = '\n'): string { + if (lines.length <= maxItems) return lines.join(sep) + return lines.slice(0, maxItems).join(sep) + `${sep}_…and ${lines.length - maxItems} more_` +} diff --git a/packages/monitor/src/runner.ts b/packages/monitor/src/runner.ts new file mode 100644 index 000000000..691e2fc49 --- /dev/null +++ b/packages/monitor/src/runner.ts @@ -0,0 +1,63 @@ +import type { MonitorOptions } from './types.ts' +import { loadSnapshot, saveSnapshot } from './s3.ts' +import { sendSlack, buildSlackBlocks } from './slack.ts' + +export async function runMonitor({ name, title, s3Key, fetchModels }: MonitorOptions): Promise { + const bucket = process.env.S3_BUCKET ?? 'models-monitor' + + console.log(`Fetching ${name} models...`) + const current = await fetchModels() + console.log(` ${current.length} models fetched`) + + console.log(`Loading snapshot from s3://${bucket}/${s3Key}...`) + const previous = await loadSnapshot(bucket, s3Key) + + if (!previous) { + console.log('No previous snapshot found — saving initial snapshot.') + await saveSnapshot(bucket, s3Key, current) + await sendSlack( + `${name} Monitor initialized — tracking ${current.length} models.`, + [{ type: 'section', text: { type: 'mrkdwn', text: `*🛰 ${name} Monitor initialized*\nTracking *${current.length} models*. Future runs will report changes.\n\`s3://${bucket}/${s3Key}\`` } }], + ) + console.log('Done.') + return + } + + console.log(` Previous snapshot: ${previous.models.length} models (${previous.timestamp})`) + + const prevMap = new Map(previous.models.map(m => [m.id, m])) + const currMap = new Map(current.map(m => [m.id, m])) + + const added = current.filter(m => !prevMap.has(m.id)) + const removed = previous.models.filter(m => !currMap.has(m.id)) + const priceChanges = current + .filter(m => { + const prev = prevMap.get(m.id) + return prev && (prev.price_prompt !== m.price_prompt || prev.price_completion !== m.price_completion) + }) + .map(m => { + const prev = prevMap.get(m.id)! + return { id: m.id, old_prompt: prev.price_prompt, new_prompt: m.price_prompt, old_completion: prev.price_completion, new_completion: m.price_completion } + }) + + console.log(` Added: ${added.length} | Removed: ${removed.length} | Price changes: ${priceChanges.length}`) + + if (!added.length && !removed.length && !priceChanges.length) { + console.log('No changes detected.') + await saveSnapshot(bucket, s3Key, current) + return + } + + const summaryParts = [ + added.length > 0 ? `${added.length} new` : null, + removed.length > 0 ? `${removed.length} removed` : null, + priceChanges.length > 0 ? `${priceChanges.length} price change(s)` : null, + ].filter(Boolean) + + await sendSlack( + `${name} update: ${summaryParts.join(', ')}`, + buildSlackBlocks(title, added, removed, priceChanges, previous.timestamp), + ) + await saveSnapshot(bucket, s3Key, current) + console.log('Snapshot updated and Slack notification sent.') +} diff --git a/packages/monitor/src/s3.ts b/packages/monitor/src/s3.ts new file mode 100644 index 000000000..5a350353f --- /dev/null +++ b/packages/monitor/src/s3.ts @@ -0,0 +1,24 @@ +import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3' +import type { ModelEntry, Snapshot } from './types.ts' + +const s3 = new S3Client({ region: process.env.AWS_REGION ?? 'eu-west-1' }) + +export async function loadSnapshot(bucket: string, key: string): Promise { + try { + const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })) + return JSON.parse(await res.Body!.transformToString()) as Snapshot + } catch (e: any) { + if (e.name === 'NoSuchKey' || e.Code === 'NoSuchKey') return null + throw e + } +} + +export async function saveSnapshot(bucket: string, key: string, models: ModelEntry[]): Promise { + const snapshot: Snapshot = { timestamp: new Date().toISOString(), models } + await s3.send(new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: JSON.stringify(snapshot, null, 2), + ContentType: 'application/json', + })) +} diff --git a/packages/monitor/src/slack.ts b/packages/monitor/src/slack.ts new file mode 100644 index 000000000..db1f985f9 --- /dev/null +++ b/packages/monitor/src/slack.ts @@ -0,0 +1,57 @@ +import type { ModelEntry, PriceChange } from './types.ts' +import { formatPrice, priceArrow, pctChange, truncateList } from './helpers.ts' + +type SlackBlock = Record + +export async function sendSlack(fallbackText: string, blocks: SlackBlock[]): Promise { + const webhookUrl = process.env.SLACK_WEBHOOK_URL ?? '' + if (!webhookUrl) { + console.log('[Slack] No SLACK_WEBHOOK_URL set — skipping notification') + return + } + const res = await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: fallbackText, blocks }), + }) + if (!res.ok) console.error(`[Slack] Webhook error: ${res.status} ${await res.text()}`) +} + +export function buildSlackBlocks( + title: string, + added: ModelEntry[], + removed: ModelEntry[], + priceChanges: PriceChange[], + previousTimestamp: string, +): SlackBlock[] { + const blocks: SlackBlock[] = [ + { type: 'section', text: { type: 'mrkdwn', text: `*${title}*` } }, + { type: 'divider' }, + ] + + if (added.length > 0) { + const lines = added.map(m => `• \`${m.id}\`\n *in:* ${formatPrice(m.price_prompt)}/1M *out:* ${formatPrice(m.price_completion)}/1M`) + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `✅ *New models (${added.length})*\n\n${truncateList(lines, 20, '\n\n')}` } }) + } + + if (removed.length > 0) { + const lines = removed.map(m => `• \`${m.id}\``) + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `❌ *Removed models (${removed.length})*\n\n${truncateList(lines, 20, '\n\n')}` } }) + } + + if (priceChanges.length > 0) { + const lines = priceChanges.map(c => { + const parts: string[] = [] + if (c.old_prompt !== c.new_prompt) + parts.push(`*in:* ${formatPrice(c.old_prompt)} ${priceArrow(c.old_prompt, c.new_prompt)} ${formatPrice(c.new_prompt)}/1M${pctChange(c.old_prompt, c.new_prompt)}`) + if (c.old_completion !== c.new_completion) + parts.push(`*out:* ${formatPrice(c.old_completion)} ${priceArrow(c.old_completion, c.new_completion)} ${formatPrice(c.new_completion)}/1M${pctChange(c.old_completion, c.new_completion)}`) + return `• \`${c.id}\`\n ${parts.join(' ')}` + }) + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `💰 *Price changes (${priceChanges.length})*\n\n${truncateList(lines, 20, '\n\n')}` } }) + } + + blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: `Checked at ${new Date().toISOString()} | Previous snapshot: ${previousTimestamp}` }] }) + + return blocks +} diff --git a/packages/monitor/src/types.ts b/packages/monitor/src/types.ts new file mode 100644 index 000000000..c68600c48 --- /dev/null +++ b/packages/monitor/src/types.ts @@ -0,0 +1,25 @@ +export interface ModelEntry { + id: string + price_prompt: number // per 1M input tokens, USD + price_completion: number // per 1M output tokens, USD +} + +export interface Snapshot { + timestamp: string + models: ModelEntry[] +} + +export interface MonitorOptions { + name: string + title: string + s3Key: string + fetchModels: () => Promise +} + +export interface PriceChange { + id: string + old_prompt: number + new_prompt: number + old_completion: number + new_completion: number +} diff --git a/packages/web/api/cron/monitor-chutes.ts b/packages/web/api/cron/monitor-chutes.ts deleted file mode 100644 index 9573397d1..000000000 --- a/packages/web/api/cron/monitor-chutes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { runMonitor, round6 } from '../../../../models-monitor/lib.ts' - -export const config = { maxDuration: 60 } - -export default async function handler(req: Request) { - if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) { - return new Response('Unauthorized', { status: 401 }) - } - - try { - await runMonitor({ - name: 'Chutes', - title: '🛰 Chutes Models Update', - s3Key: 'chutes/snapshot.json', - async fetchModels() { - const headers: Record = {} - if (process.env.CHUTES_API_KEY) headers['Authorization'] = `Bearer ${process.env.CHUTES_API_KEY}` - - const res = await fetch('https://llm.chutes.ai/v1/models', { headers }) - if (!res.ok) throw new Error(`Chutes API error: ${res.status}`) - const data = await res.json() as { data: Array<{ id: string; pricing: { prompt: number; completion: number } }> } - return data.data.map(m => ({ - id: m.id, - price_prompt: round6(m.pricing.prompt), - price_completion: round6(m.pricing.completion), - })) - }, - }) - return new Response('ok') - } catch (err) { - console.error(err) - return new Response(String(err), { status: 500 }) - } -} diff --git a/packages/web/api/cron/monitor-openrouter.ts b/packages/web/api/cron/monitor-openrouter.ts deleted file mode 100644 index bd08cde56..000000000 --- a/packages/web/api/cron/monitor-openrouter.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { runMonitor, round6 } from '../../../../models-monitor/lib.ts' - -export const config = { maxDuration: 60 } - -export default async function handler(req: Request) { - if (req.headers.get('authorization') !== `Bearer ${process.env.CRON_SECRET}`) { - return new Response('Unauthorized', { status: 401 }) - } - - try { - await runMonitor({ - name: 'OpenRouter', - title: '🛰 OpenRouter Models Update', - s3Key: 'openrouter/snapshot.json', - async fetchModels() { - const res = await fetch('https://openrouter.ai/api/v1/models') - if (!res.ok) throw new Error(`OpenRouter API error: ${res.status}`) - const data = await res.json() as { data: Array<{ id: string; pricing: { prompt: string; completion: string } }> } - return data.data.map(m => ({ - id: m.id, - price_prompt: round6(parseFloat(m.pricing.prompt) * 1_000_000), - price_completion: round6(parseFloat(m.pricing.completion) * 1_000_000), - })) - }, - }) - return new Response('ok') - } catch (err) { - console.error(err) - return new Response(String(err), { status: 500 }) - } -}