Преглед изворни кода

feat: update pricing schema for models to ensure more accurate cost tracking (#27184)

Aiden Cline пре 3 месеци
родитељ
комит
c2b1ebd9dc

+ 12 - 0
packages/opencode/src/provider/models.ts

@@ -10,11 +10,23 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
 import { withTransientReadRetry } from "@/util/effect-http-client"
 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({
   input: Schema.Finite,
   output: Schema.Finite,
   cache_read: Schema.optional(Schema.Finite),
   cache_write: Schema.optional(Schema.Finite),
+  tiers: Schema.optional(Schema.Array(CostTier)),
   context_over_200k: Schema.optional(
     Schema.Struct({
       input: Schema.Finite,

+ 22 - 0
packages/opencode/src/provider/provider.ts

@@ -869,10 +869,21 @@ const ProviderCacheCost = Schema.Struct({
   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({
   input: Schema.Finite,
   output: Schema.Finite,
   cache: ProviderCacheCost,
+  tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)),
   experimentalOver200K: optionalOmitUndefined(
     Schema.Struct({
       input: Schema.Finite,
@@ -977,6 +988,17 @@ function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
       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) {
     result.experimentalOver200K = {
       cache: {

+ 6 - 2
packages/opencode/src/session/session.ts

@@ -418,10 +418,14 @@ export const getUsage = (input: { model: Provider.Model; usage: LanguageModelUsa
     },
   }
 
+  const contextTokens = inputTokens
   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
+      : input.model.cost)
   return {
     cost: safe(
       new Decimal(0)

+ 95 - 0
packages/opencode/test/session/compaction.test.ts

@@ -1758,6 +1758,101 @@ describe("SessionNs.getUsage", () => {
     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"])(
     "computes total from components for %s models",
     (npm) => {

+ 22 - 3
packages/opencode/test/session/llm.test.ts

@@ -277,6 +277,25 @@ async function loadFixture(providerID: string, modelID: string) {
   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) {
   const lines = chunks.map((chunk) => `data: ${typeof chunk === "string" ? chunk : JSON.stringify(chunk)}`)
   if (includeDone) {
@@ -617,7 +636,7 @@ describe("session.llm.stream", () => {
                 npm: "@ai-sdk/openai",
                 api: "https://api.openai.com/v1",
                 models: {
-                  [model.id]: model,
+                  [model.id]: configModel(model),
                 },
                 options: {
                   apiKey: "test-openai-key",
@@ -733,7 +752,7 @@ describe("session.llm.stream", () => {
                 npm: "@ai-sdk/openai",
                 api: "https://api.openai.com/v1",
                 models: {
-                  [model.id]: model,
+                  [model.id]: configModel(model),
                 },
                 options: {
                   apiKey: "test-openai-key",
@@ -970,7 +989,7 @@ describe("session.llm.stream", () => {
                 npm: "@ai-sdk/anthropic",
                 api: "https://api.anthropic.com/v1",
                 models: {
-                  [model.id]: model,
+                  [model.id]: configModel(model),
                 },
                 options: {
                   apiKey: "test-anthropic-key",

Разлика између датотеке није приказан због своје велике величине
+ 970 - 713
packages/opencode/test/tool/fixtures/models-api.json


Неке датотеке нису приказане због велике количине промена