Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.203.0",
"@opentelemetry/resources": "^2.0.1",
"@pydantic/genai-prices": "~0.0.36",
"@pydantic/genai-prices": "~0.0.38",
"@pydantic/logfire-api": "^0.9.0",
"eventsource-parser": "^3.0.6",
"mime-types": "^3.0.1",
Expand Down
2 changes: 2 additions & 0 deletions gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { KeysDb, LimitDb } from './db'
import { gateway } from './gateway'
import type { DefaultProviderProxy, Middleware, Next } from './providers/default'
import type { RateLimiter } from './rateLimiter'
import { refreshGenaiPrices } from './refreshGenaiPrices'
import type { SubFetch } from './types'
import { ctHeader, response405, runAfter, textResponse } from './utils'

Expand Down Expand Up @@ -48,6 +49,7 @@ export async function gatewayFetch(
ctx: ExecutionContext,
options: GatewayOptions,
): Promise<Response> {
ctx.waitUntil(refreshGenaiPrices())
Copy link

@alexmojaki alexmojaki Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused how this is non-blocking. refreshGenaiPrices() returns waitForUpdate() which returns providerDataPromise which should be freshDataPromise.

let { pathname: proxyPath, search: queryString } = url
if (options.proxyPrefixLength) {
proxyPath = proxyPath.slice(options.proxyPrefixLength)
Expand Down
54 changes: 54 additions & 0 deletions gateway/src/refreshGenaiPrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type Provider, updatePrices, waitForUpdate } from '@pydantic/genai-prices'

// data will be refetched every 30 minutes
const PRICE_TTL = 1000 * 60 * 30
let genaiDataTimestamp: number | null = null
let isFetching = false

export function refreshGenaiPrices() {
updatePrices(({ setProviderData, remoteDataUrl }) => {
if (genaiDataTimestamp !== null) {
console.debug('genai prices in-memory cache found')

if (Date.now() - genaiDataTimestamp < PRICE_TTL) {
// this will be the most frequent, cheap path
console.debug('genai prices in-memory data is fresh')
return
} else {
console.debug('genai prices in-memory cache is stale, attempting to fetch remote data')
}
}

if (isFetching) {
console.debug('genai-prices data fetch already in progress, skipping')
return
}

console.debug('Fetching genai-prices data')
isFetching = true

// Note: **DO NOT** await this promise
const freshDataPromise = fetch(remoteDataUrl)
.then(async (response) => {
if (!response.ok) {
console.error('Failed fetching provider data, response status %d', response.status)
return null
}

const freshData = (await response.json()) as Provider[]
console.debug('Updated genai prices data, %d providers', freshData.length)
genaiDataTimestamp = Date.now()
return freshData
})
.catch((error: unknown) => {
console.error('Failed fetching provider data err: %o', error)
return null
})
.finally(() => {
isFetching = false
})

setProviderData(freshDataPromise)
})
return waitForUpdate()
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.