mirror of
https://github.com/anomalyco/opencode.git
synced 2026-06-01 19:05:38 +00:00
feat: update pricing schema for models to ensure more accurate cost tracking (#27184)
This commit is contained in:
@@ -10,11 +10,23 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
|||||||
import { withTransientReadRetry } from "@/util/effect-http-client"
|
import { withTransientReadRetry } from "@/util/effect-http-client"
|
||||||
import { CatalogModelStatus } from "./model-status"
|
import { CatalogModelStatus } from "./model-status"
|
||||||
|
|
||||||
|
const CostTier = Schema.Struct({
|
||||||
|
input: Schema.Finite,
|
||||||
|
output: Schema.Finite,
|
||||||
|
cache_read: Schema.optional(Schema.Finite),
|
||||||
|
cache_write: Schema.optional(Schema.Finite),
|
||||||
|
tier: Schema.Struct({
|
||||||
|
type: Schema.Literal("context"),
|
||||||
|
size: Schema.Finite,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const Cost = Schema.Struct({
|
const Cost = Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Schema.Finite,
|
||||||
output: Schema.Finite,
|
output: Schema.Finite,
|
||||||
cache_read: Schema.optional(Schema.Finite),
|
cache_read: Schema.optional(Schema.Finite),
|
||||||
cache_write: Schema.optional(Schema.Finite),
|
cache_write: Schema.optional(Schema.Finite),
|
||||||
|
tiers: Schema.optional(Schema.Array(CostTier)),
|
||||||
context_over_200k: Schema.optional(
|
context_over_200k: Schema.optional(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Schema.Finite,
|
||||||
|
|||||||
@@ -869,10 +869,21 @@ const ProviderCacheCost = Schema.Struct({
|
|||||||
write: Schema.Finite,
|
write: Schema.Finite,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const ProviderCostTier = Schema.Struct({
|
||||||
|
input: Schema.Finite,
|
||||||
|
output: Schema.Finite,
|
||||||
|
cache: ProviderCacheCost,
|
||||||
|
tier: Schema.Struct({
|
||||||
|
type: Schema.Literal("context"),
|
||||||
|
size: Schema.Finite,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const ProviderCost = Schema.Struct({
|
const ProviderCost = Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Schema.Finite,
|
||||||
output: Schema.Finite,
|
output: Schema.Finite,
|
||||||
cache: ProviderCacheCost,
|
cache: ProviderCacheCost,
|
||||||
|
tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
|
||||||
experimentalOver200K: optionalOmitUndefined(
|
experimentalOver200K: optionalOmitUndefined(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
input: Schema.Finite,
|
input: Schema.Finite,
|
||||||
@@ -977,6 +988,17 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
|
|||||||
write: c?.cache_write ?? 0,
|
write: c?.cache_write ?? 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if (c?.tiers) {
|
||||||
|
result.tiers = c.tiers.map((item) => ({
|
||||||
|
input: item.input,
|
||||||
|
output: item.output,
|
||||||
|
cache: {
|
||||||
|
read: item.cache_read ?? 0,
|
||||||
|
write: item.cache_write ?? 0,
|
||||||
|
},
|
||||||
|
tier: item.tier,
|
||||||
|
}))
|
||||||
|
}
|
||||||
if (c?.context_over_200k) {
|
if (c?.context_over_200k) {
|
||||||
result.experimentalOver200K = {
|
result.experimentalOver200K = {
|
||||||
cache: {
|
cache: {
|
||||||
|
|||||||
@@ -418,10 +418,14 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contextTokens = inputTokens
|
||||||
const costInfo =
|
const costInfo =
|
||||||
input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000
|
input.model.cost?.tiers
|
||||||
|
?.filter((item) => item.tier.type === "context" && contextTokens > item.tier.size)
|
||||||
|
.sort((a, b) => b.tier.size - a.tier.size)[0] ??
|
||||||
|
(input.model.cost?.experimentalOver200K && contextTokens > 200_000
|
||||||
? input.model.cost.experimentalOver200K
|
? input.model.cost.experimentalOver200K
|
||||||
: input.model.cost
|
: input.model.cost)
|
||||||
return {
|
return {
|
||||||
cost: safe(
|
cost: safe(
|
||||||
new Decimal(0)
|
new Decimal(0)
|
||||||
|
|||||||
@@ -1758,6 +1758,101 @@ describe("SessionNs.getUsage", () => {
|
|||||||
expect(result.cost).toBe(3 + 1.5)
|
expect(result.cost).toBe(3 + 1.5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("uses matching context cost tier before over-200k fallback", () => {
|
||||||
|
const model = createModel({
|
||||||
|
context: 1_000_000,
|
||||||
|
output: 32_000,
|
||||||
|
cost: {
|
||||||
|
input: 1,
|
||||||
|
output: 2,
|
||||||
|
cache: { read: 0.1, write: 0.5 },
|
||||||
|
tiers: [
|
||||||
|
{
|
||||||
|
input: 3,
|
||||||
|
output: 4,
|
||||||
|
cache: { read: 0.3, write: 1.5 },
|
||||||
|
tier: { type: "context", size: 200_000 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 5,
|
||||||
|
output: 6,
|
||||||
|
cache: { read: 0.5, write: 2.5 },
|
||||||
|
tier: { type: "context", size: 500_000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
experimentalOver200K: {
|
||||||
|
input: 100,
|
||||||
|
output: 100,
|
||||||
|
cache: { read: 100, write: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = SessionNs.getUsage({
|
||||||
|
model,
|
||||||
|
usage: {
|
||||||
|
inputTokens: 650_000,
|
||||||
|
outputTokens: 100_000,
|
||||||
|
totalTokens: 750_000,
|
||||||
|
inputTokenDetails: {
|
||||||
|
noCacheTokens: undefined,
|
||||||
|
cacheReadTokens: 100_000,
|
||||||
|
cacheWriteTokens: undefined,
|
||||||
|
},
|
||||||
|
outputTokenDetails: {
|
||||||
|
textTokens: undefined,
|
||||||
|
reasoningTokens: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.tokens.input).toBe(550_000)
|
||||||
|
expect(result.cost).toBe(2.75 + 0.6 + 0.05)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to over-200k pricing when no cost tier matches", () => {
|
||||||
|
const model = createModel({
|
||||||
|
context: 1_000_000,
|
||||||
|
output: 32_000,
|
||||||
|
cost: {
|
||||||
|
input: 1,
|
||||||
|
output: 2,
|
||||||
|
cache: { read: 0.1, write: 0.5 },
|
||||||
|
tiers: [
|
||||||
|
{
|
||||||
|
input: 5,
|
||||||
|
output: 6,
|
||||||
|
cache: { read: 0.5, write: 2.5 },
|
||||||
|
tier: { type: "context", size: 500_000 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
experimentalOver200K: {
|
||||||
|
input: 3,
|
||||||
|
output: 4,
|
||||||
|
cache: { read: 0.3, write: 1.5 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = SessionNs.getUsage({
|
||||||
|
model,
|
||||||
|
usage: {
|
||||||
|
inputTokens: 300_000,
|
||||||
|
outputTokens: 100_000,
|
||||||
|
totalTokens: 400_000,
|
||||||
|
inputTokenDetails: {
|
||||||
|
noCacheTokens: undefined,
|
||||||
|
cacheReadTokens: undefined,
|
||||||
|
cacheWriteTokens: undefined,
|
||||||
|
},
|
||||||
|
outputTokenDetails: {
|
||||||
|
textTokens: undefined,
|
||||||
|
reasoningTokens: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.cost).toBe(0.9 + 0.4)
|
||||||
|
})
|
||||||
|
|
||||||
test.each(["@ai-sdk/anthropic", "@ai-sdk/amazon-bedrock", "@ai-sdk/google-vertex/anthropic"])(
|
test.each(["@ai-sdk/anthropic", "@ai-sdk/amazon-bedrock", "@ai-sdk/google-vertex/anthropic"])(
|
||||||
"computes total from components for %s models",
|
"computes total from components for %s models",
|
||||||
(npm) => {
|
(npm) => {
|
||||||
|
|||||||
@@ -277,6 +277,25 @@ async function loadFixture(providerID: string, modelID: string) {
|
|||||||
return { provider, model }
|
return { provider, model }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configModel(model: ModelsDev.Model) {
|
||||||
|
return {
|
||||||
|
id: model.id,
|
||||||
|
name: model.name,
|
||||||
|
family: model.family,
|
||||||
|
release_date: model.release_date,
|
||||||
|
attachment: model.attachment,
|
||||||
|
reasoning: model.reasoning,
|
||||||
|
temperature: model.temperature,
|
||||||
|
tool_call: model.tool_call,
|
||||||
|
interleaved: model.interleaved,
|
||||||
|
cost: model.cost ? { ...model.cost, tiers: undefined } : undefined,
|
||||||
|
limit: model.limit,
|
||||||
|
modalities: model.modalities,
|
||||||
|
status: model.status,
|
||||||
|
provider: model.provider,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createEventStream(chunks: unknown[], includeDone = false) {
|
function createEventStream(chunks: unknown[], includeDone = false) {
|
||||||
const lines = chunks.map((chunk) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}`)
|
const lines = chunks.map((chunk) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}`)
|
||||||
if (includeDone) {
|
if (includeDone) {
|
||||||
@@ -617,7 +636,7 @@ describe("session.llm.stream", () => {
|
|||||||
npm: "@ai-sdk/openai",
|
npm: "@ai-sdk/openai",
|
||||||
api: "https://api.openai.com/v1",
|
api: "https://api.openai.com/v1",
|
||||||
models: {
|
models: {
|
||||||
[model.id]: model,
|
[model.id]: configModel(model),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
apiKey: "test-openai-key",
|
apiKey: "test-openai-key",
|
||||||
@@ -733,7 +752,7 @@ describe("session.llm.stream", () => {
|
|||||||
npm: "@ai-sdk/openai",
|
npm: "@ai-sdk/openai",
|
||||||
api: "https://api.openai.com/v1",
|
api: "https://api.openai.com/v1",
|
||||||
models: {
|
models: {
|
||||||
[model.id]: model,
|
[model.id]: configModel(model),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
apiKey: "test-openai-key",
|
apiKey: "test-openai-key",
|
||||||
@@ -970,7 +989,7 @@ describe("session.llm.stream", () => {
|
|||||||
npm: "@ai-sdk/anthropic",
|
npm: "@ai-sdk/anthropic",
|
||||||
api: "https://api.anthropic.com/v1",
|
api: "https://api.anthropic.com/v1",
|
||||||
models: {
|
models: {
|
||||||
[model.id]: model,
|
[model.id]: configModel(model),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
apiKey: "test-anthropic-key",
|
apiKey: "test-anthropic-key",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user