Skip to content

Existing user costs silently block pricing updates from new package versions #3

@s2x

Description

@s2x

Description

getModelCost() in models.config.mjs:143-157 unconditionally prefers costs from the user's existing config file over hardcoded SUPPORTED_MODELS pricing:

export function getModelCost(displayName, existingCosts) {
  // Always prefer existing costs from config (user's custom values)
  if (existingCosts && existingCosts[displayName]) {
    return existingCosts[displayName];   // <-- always wins
  }
  
  // Fall back to hardcoded defaults if available
  const config = getModelConfig(displayName);
  if (config?.cost) {
    return clone(config.cost);
  }
  
  return clone(DEFAULT_FALLBACK_COSTS);
}

Scenario

  1. User runs opencode-nexos-models-config v1.17.0 → costs for "Claude Sonnet 4.5" are written to opencode.json as { input: 3.3, output: 16.5, ... }.
  2. Anthropic changes pricing. Package v1.18.0 updates SUPPORTED_MODELS["Claude Sonnet 4.5"].cost to { input: 3.0, output: 15.0, ... }.
  3. User upgrades to v1.18.0 and re-runs the tool.
  4. Bug: getExistingModelCosts() loads the old { input: 3.3, output: 16.5 } from the config file. getModelCost() returns these stale values. The updated pricing from v1.18.0 is silently ignored.

Impact

  • Users who previously ran the tool never get pricing updates unless they manually delete their config or use --custom-costs to override.
  • No warning is shown that existing costs differ from the package's built-in pricing.
  • This defeats the purpose of updating SUPPORTED_MODELS costs in new releases.

Distinction: custom vs. stale costs

The current logic cannot distinguish between:

  • User-customized costs (set via --custom-costs) — should be preserved.
  • Auto-generated costs from a previous run — should be updated.

Proposed fix

Option A: Always use hardcoded costs for supported models, only use existing costs for unsupported models:

export function getModelCost(displayName, existingCosts) {
  const config = getModelConfig(displayName);
  if (config?.cost) {
    return clone(config.cost);  // hardcoded always wins for supported models
  }
  if (existingCosts && existingCosts[displayName]) {
    return existingCosts[displayName];
  }
  return clone(DEFAULT_FALLBACK_COSTS);
}

Option B: Track custom vs. auto-generated with a flag (e.g. "_customCost": true) and only preserve flagged entries.

Option C: Warn the user when existing costs differ from built-in pricing and ask whether to update.

Acceptance criteria

  • When a supported model's pricing is updated in SUPPORTED_MODELS, re-running the tool produces the new pricing in the config file.
  • User-customized costs (set via --custom-costs) are still preserved.
  • A clear migration path exists for users upgrading from older versions.

Test cases

test("should use updated hardcoded cost over stale existing cost for supported models", () => {
  // Simulate: existing config has old pricing
  const existingCosts = {
    "Claude Sonnet 4.5": { input: 99.99, output: 99.99, cache_read: 99.99 }
  };
  
  const cost = getModelCost("Claude Sonnet 4.5", existingCosts);
  
  // Should return the hardcoded SUPPORTED_MODELS cost, not the stale one
  expect(cost).toEqual(SUPPORTED_MODELS["Claude Sonnet 4.5"].cost);
  expect(cost.input).not.toBe(99.99);
});

test("should preserve existing costs for unsupported models", () => {
  const existingCosts = {
    "Custom Model": { input: 10, output: 20 }
  };
  
  const cost = getModelCost("Custom Model", existingCosts);
  
  expect(cost).toEqual({ input: 10, output: 20 });
});

test("should preserve user custom costs set via --custom-costs", () => {
  // This test depends on the chosen implementation approach
  // For Option B: check that _customCost flag is respected
});

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions