Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

202 lines
5.8 KiB
TypeScript

/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
addOpenAIUsage,
buildOpenAIUsageCost,
buildOpenAIUsageTokens,
createOpenAIUsageAccumulator,
} from '@/providers/openai/usage'
import type { ResponsesUsageTokens } from '@/providers/openai/utils'
import { calculateCost } from '@/providers/utils'
/** input $2.50/M, cachedInput $1.25/M, output $10.00/M. */
const MODEL = 'gpt-4o'
/** input $2.50/M, cachedInput $0.25/M, output $15.00/M — bills cache writes. */
const CACHE_WRITE_MODEL = 'gpt-5.6-terra'
/**
* Builds a Responses usage payload. `promptTokens` is inclusive of cached and
* written tokens, matching what {@link parseResponsesUsage} emits.
*/
function responsesUsage(partial: Partial<ResponsesUsageTokens>): ResponsesUsageTokens {
const promptTokens = partial.promptTokens ?? 0
const completionTokens = partial.completionTokens ?? 0
return {
promptTokens,
completionTokens,
totalTokens: partial.totalTokens ?? promptTokens + completionTokens,
cachedTokens: partial.cachedTokens ?? 0,
cacheWriteTokens: partial.cacheWriteTokens ?? 0,
reasoningTokens: partial.reasoningTokens ?? 0,
}
}
describe('OpenAI usage aggregation', () => {
it('matches plain list pricing when nothing was cached', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(usage, responsesUsage({ promptTokens: 12_345, completionTokens: 6_789 }))
const uncached = calculateCost(MODEL, 12_345, 6_789)
expect(buildOpenAIUsageTokens(usage)).toEqual({
input: 12_345,
output: 6_789,
total: 19_134,
cacheRead: 0,
cacheWrite: 0,
})
expect(buildOpenAIUsageCost(MODEL, usage)).toMatchObject({
input: uncached.input,
output: uncached.output,
total: uncached.total,
})
})
it('bills cached tokens at the cached rate instead of the full input rate', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(
usage,
responsesUsage({ promptTokens: 1_000_000, cachedTokens: 600_000, completionTokens: 0 })
)
/** 400k uncached at $2.50/M plus 600k cached at $1.25/M. */
expect(buildOpenAIUsageCost(MODEL, usage)).toMatchObject({
input: 1.75,
output: 0,
total: 1.75,
})
expect(calculateCost(MODEL, 1_000_000, 0).input).toBe(2.5)
})
it('reports cache reads separately while keeping the prompt total intact', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(
usage,
responsesUsage({ promptTokens: 1_000, cachedTokens: 800, completionTokens: 100 })
)
expect(buildOpenAIUsageTokens(usage)).toEqual({
input: 200,
output: 100,
total: 1_100,
cacheRead: 800,
cacheWrite: 0,
})
})
it('bills GPT-5.6 cache writes at 1.25x the uncached input rate', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(
usage,
responsesUsage({
promptTokens: 1_000_000,
cacheWriteTokens: 1_000_000,
completionTokens: 0,
})
)
/** 1M written at $2.50/M x 1.25. */
expect(buildOpenAIUsageCost(CACHE_WRITE_MODEL, usage)).toMatchObject({
input: 3.125,
output: 0,
total: 3.125,
})
})
it('aggregates uncached, cached, written, and output tokens in one turn', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(
usage,
responsesUsage({
promptTokens: 1_000_000,
cachedTokens: 600_000,
cacheWriteTokens: 200_000,
completionTokens: 100_000,
})
)
expect(buildOpenAIUsageTokens(usage)).toEqual({
input: 200_000,
output: 100_000,
total: 1_100_000,
cacheRead: 600_000,
cacheWrite: 200_000,
})
/** 0.5 uncached + 0.15 cached + 0.625 written input, 1.5 output. */
expect(buildOpenAIUsageCost(CACHE_WRITE_MODEL, usage)).toMatchObject({
input: 1.275,
output: 1.5,
total: 2.775,
})
})
it('accumulates each tool-loop turn exactly once', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(usage, responsesUsage({ promptTokens: 1_000, completionTokens: 100 }))
addOpenAIUsage(
usage,
responsesUsage({ promptTokens: 2_000, cachedTokens: 1_500, completionTokens: 200 })
)
expect(buildOpenAIUsageTokens(usage)).toEqual({
input: 1_500,
output: 300,
total: 3_300,
cacheRead: 1_500,
cacheWrite: 0,
})
expect(buildOpenAIUsageCost(MODEL, usage)).toMatchObject({
input: 0.005625,
output: 0.003,
total: 0.008625,
})
})
it('ignores turns that reported no usage', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(usage, responsesUsage({ promptTokens: 1_000, completionTokens: 100 }))
addOpenAIUsage(usage, undefined)
expect(buildOpenAIUsageTokens(usage)).toEqual({
input: 1_000,
output: 100,
total: 1_100,
cacheRead: 0,
cacheWrite: 0,
})
})
it('adds tool cost to the total and only reports the field when charged', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(usage, responsesUsage({ promptTokens: 1_000_000, completionTokens: 0 }))
expect(buildOpenAIUsageCost(MODEL, usage, 0.25)).toMatchObject({
input: 2.5,
total: 2.75,
toolCost: 0.25,
})
expect(buildOpenAIUsageCost(MODEL, usage)).not.toHaveProperty('toolCost')
})
it('does not charge for cache tokens a vendor payload over-reported', () => {
const usage = createOpenAIUsageAccumulator()
addOpenAIUsage(
usage,
responsesUsage({
promptTokens: 1_000,
cachedTokens: 900,
cacheWriteTokens: 400,
completionTokens: 0,
})
)
expect(buildOpenAIUsageTokens(usage)).toMatchObject({
input: 0,
cacheRead: 900,
cacheWrite: 100,
})
})
})