mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
fix: count OpenRouter cache writes and drop unverifiable rates
This commit is contained in:
@@ -190,6 +190,12 @@ pub const MAX_MODEL_RATE: f64 = 1000.0;
|
||||
/// which never deserializes it into `AIConfig`, so the typed check on the
|
||||
/// workspace path does not cover it.
|
||||
pub fn validate_model_pricing_json(ai_config: &serde_json::Value) -> Result<(), String> {
|
||||
// The container itself has to be checked too: a non-object `ai_config` persists
|
||||
// here and then fails to deserialize as `AIConfig`, which drops the whole
|
||||
// instance config back to its default for every workspace inheriting it.
|
||||
if !ai_config.is_null() && !ai_config.is_object() {
|
||||
return Err("ai_config must be an object".to_string());
|
||||
}
|
||||
let pricing = match ai_config.get("model_pricing") {
|
||||
None | Some(serde_json::Value::Null) => return Ok(()),
|
||||
// A present-but-wrong shape must be rejected, not skipped: it would persist
|
||||
|
||||
@@ -193,7 +193,12 @@ export function openAICompletionsUsageToChatTokenUsage(
|
||||
prompt_tokens?: number | null
|
||||
completion_tokens?: number | null
|
||||
total_tokens?: number | null
|
||||
prompt_tokens_details?: { cached_tokens?: number | null } | null
|
||||
prompt_tokens_details?: {
|
||||
cached_tokens?: number | null
|
||||
/** OpenRouter surfaces Anthropic's cache creation here; OpenAI, whose
|
||||
* caching is automatic and unbilled, reports no such field. */
|
||||
cache_write_tokens?: number | null
|
||||
} | null
|
||||
/** OpenRouter reports what it actually charged when the request opts in. */
|
||||
cost?: number | null
|
||||
}
|
||||
@@ -208,7 +213,7 @@ export function openAICompletionsUsageToChatTokenUsage(
|
||||
completion,
|
||||
total: usage?.total_tokens ?? prompt + completion,
|
||||
cacheRead: usage?.prompt_tokens_details?.cached_tokens ?? 0,
|
||||
cacheWrite: 0,
|
||||
cacheWrite: usage?.prompt_tokens_details?.cache_write_tokens ?? 0,
|
||||
...(typeof usage?.cost === 'number' ? { cost: usage.cost } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,14 @@ export function buildModelMatchers<T>(
|
||||
// its own price, not another route to this one — so under strictVariants an
|
||||
// entry does not match when a further *name* segment follows. What follows
|
||||
// is only a decoration when it is a date (`-20251101`), Bedrock's `-v1`, or
|
||||
// one of the alias words below (`claude-3-5-haiku-latest` is the same model
|
||||
// as `claude-3-5-haiku`, and is a shipped default). Off by default: for a
|
||||
// context window an inherited value is a safe approximation, for a price it
|
||||
// is a wrong number.
|
||||
strictVariants ? `(?!-(?!v\\d|${DECORATIVE_SUFFIXES.join('|')})[a-z])` : ''
|
||||
// one of the alias words below, at the very end of the id
|
||||
// (`claude-3-5-haiku-latest` is the same model as `claude-3-5-haiku`, and is
|
||||
// a shipped default; `gpt-5-preview-pro` would be a different one again).
|
||||
// Off by default: for a context window an inherited value is a safe
|
||||
// approximation, for a price it is a wrong number.
|
||||
strictVariants
|
||||
? `(?!-(?!(?:v\\d|${DECORATIVE_SUFFIXES.join('|')})$)[a-z])`
|
||||
: ''
|
||||
].join('')
|
||||
return [new RegExp(pattern + guards), value]
|
||||
})
|
||||
|
||||
@@ -53,8 +53,10 @@ describe('resolveModelPrice', () => {
|
||||
expect(
|
||||
resolveModelPrice('openrouter', '~anthropic/claude-sonnet-latest', undefined)?.price.input
|
||||
).toBe(3)
|
||||
// …while a genuine sub-model stays unpriced.
|
||||
// …while a genuine sub-model stays unpriced, including one hiding behind a
|
||||
// decoration.
|
||||
expect(resolveModelPrice('openai', 'gpt-5-pro', undefined)).toBeUndefined()
|
||||
expect(resolveModelPrice('openai', 'gpt-5-preview-pro', undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('prefers a workspace override, keeping the model’s own cache ratios', () => {
|
||||
|
||||
@@ -110,13 +110,13 @@ const MODEL_PRICES: [name: string, price: PriceEntry | null][] = [
|
||||
['o4-mini', { input: 1.1, output: 4.4, cacheRead: 0.275 }],
|
||||
['o3-mini', { input: 1.1, output: 4.4, cacheRead: 0.55 }],
|
||||
['o3', { input: 2, output: 8, cacheRead: 0.5 }],
|
||||
// Google — a cached read is a quarter of input across the 2.5 family; the 3.x
|
||||
// families are not tracked
|
||||
// Google — left unpriced. Gemini's cached-input rates do not follow a single
|
||||
// ratio and its Pro tier charges more above a context threshold, which a flat
|
||||
// per-model rate cannot express; an estimate here would be wrong in a direction
|
||||
// nobody can see. A workspace that runs Gemini sets its own rates.
|
||||
['gemini-3.1', null],
|
||||
['gemini-3', null],
|
||||
['gemini-2.5-flash-lite', { input: 0.1, output: 0.4, cacheRead: 0.025 }],
|
||||
['gemini-2.5-flash', { input: 0.3, output: 2.5, cacheRead: 0.075 }],
|
||||
['gemini-2.5-pro', { input: 1.25, output: 10, cacheRead: 0.31 }]
|
||||
['gemini-2.5', null]
|
||||
]
|
||||
|
||||
const MODEL_PRICE_MATCHERS = buildModelMatchers(
|
||||
|
||||
Reference in New Issue
Block a user