Skip to content

feat(pricing): tiered cost accounting, model-ID normalization, and live price feeds - #9

Merged
hallelx2 merged 2 commits into
mainfrom
halleluyaholudele/hal-529-pricing
Aug 2, 2026
Merged

feat(pricing): tiered cost accounting, model-ID normalization, and live price feeds#9
hallelx2 merged 2 commits into
mainfrom
halleluyaholudele/hal-529-pricing

Conversation

@hallelx2

@hallelx2 hallelx2 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Stacked on #8. Base is that branch, not main — retarget to main once #8 merges.

The reported cost was wrong in four independent ways. All four are fixed together because they compound: any one left in place still leaves the dollar figure untrustworthy.

1. Cache and reasoning tokens were dropped entirely

langchaingo already surfaces CacheCreationInputTokens, CacheReadInputTokens, PromptCachedTokens and ReasoningTokens. llmgate read none of them.

Provider Before Effect
Anthropic / GLM cache tokens reported outside input_tokens, so never counted cache writes bill at 1.25x input and were charged $0 — understated
OpenAI cached tokens counted inside prompt_tokens at full rate cached input costs 0.5x — overstated
Google same, inside the prompt count cached content costs 0.25x — overstated

Usage now carries CacheWriteTokens, CacheReadTokens and ReasoningTokens, normalized to a disjoint form so Input + CacheWrite + CacheRead is the whole prompt on every provider. Price gains the matching tiers, with family-specific fallbacks when a table entry predates cache pricing (Anthropic 1.25x/0.1x, OpenAI 1.0x/0.5x, Google 1.0x/0.25x — one global default would be wrong for two of the three).

2. Lookup was an exact map hit

Every ID a real API actually returns missed the table and priced at $0:

claude-sonnet-4-5-20250929        the dated ID Anthropic returns
models/gemini-2.5-flash           Google's own SDK prefix
us.anthropic.claude-opus-4-1-v1:0 Bedrock
z-ai/glm-4.6                      how the price feeds key the model we run

Canonical strips vendor/region prefixes, snapshot dates and revisions, then falls back to a longest-prefix match. Longest matters: claude-sonnet-4 also prefixes claude-sonnet-4-5-preview, and the shorter key would bill Sonnet 4.5 at Sonnet 4 rates. Prefix matches must also land on a segment boundary, so glm-4.5 can't absorb glm-4.55.

Rates are keyed by model ID alone, never provider+model — vectorless runs GLM-4.6 through z.ai's Anthropic-compatible gateway, and a provider-scoped key would look up anthropic/glm-4.6 and miss.

3. Missing usage was reported as a priced $0 call

With no counts in GenerationInfo, ComputeWithOK(model, 0, 0) returned (0, true)Priced: true positively asserting that a call which returned content was free. Worse than the unpriced case, which at least says "unknown".

Usage gains TokensReported and Estimated; the adapter estimates from the tokenizer and labels it rather than reporting a zero.

4. The table drifted silently

UseRemote layers a refreshed snapshot from LiteLLM or OpenRouter over the embedded defaults, beneath any Register overrides.

Opt-in and staying that way — importing pricing does no network I/O. It fails open at every layer: lookups never touch the network, a failed fetch keeps the last snapshot, snapshots persist to disk so restarts start warm, and a snapshot whose rates moved >10x from the embedded values is rejected as a units error rather than adopted. That last gate is the one that matters — both feeds quote per-token where we work in per-Mtok, so a conversion slip is a factor of a million and invisible in a diff. perMTok is one named function with its own test for the same reason.

Verification

  • go build, go vet, golangci-lint, staticcheck — clean
  • 45 tests, no skips. Run 3x with -shuffle=on to prove no order dependence, after adding pricing.Unregister so the layering test can undo its own override
  • Zero network calls in go test — both feeds are exercised against httptest fixtures that mirror the real payloads, including OpenRouter's string-encoded rates and LiteLLM's non-model sample_spec key
  • -race still not runnable locally (no gcc); CI covers it

Notable tests: TestAnthropicCacheWriteIsBilled (the $0 regression), TestOpenAICachedTokensSubtracted (the double-charge), TestLongestPrefixWins, TestMissingUsageIsEstimatedNotZero, TestRemoteRejectsImplausibleRates, TestGLMUsageThroughAnthropicGateway.

Known gap

Gemini 2.5 thinking tokens land in the API's thoughtsTokenCount, which langchaingo does not surface at all — they remain invisible and unbilled. That needs the native client work (HAL-539) and is tracked on HAL-529 so it isn't mistaken for a zero.

Closes HAL-529
Closes HAL-530
Closes HAL-531
Closes HAL-532

Summary by Sourcery

Improve pricing accuracy and robustness by introducing tiered token cost accounting, canonicalized model ID resolution, and optional live price feeds, and by surfacing richer usage metadata in responses.

New Features:

  • Add support for cache write/read and reasoning token pricing via a new Tokens model and tiered rates, including family-specific cache multipliers.
  • Normalize provider-returned model IDs to canonical base IDs with prefix/date/revision handling and longest-prefix matching, ensuring variants and gateway-served models are correctly priced.
  • Expose detailed per-call usage breakdown in responses, including cache and reasoning tokens plus flags indicating whether usage was reported or estimated.
  • Introduce an opt-in remote pricing layer that fetches, validates, caches, and periodically refreshes rates from community sources like LiteLLM and OpenRouter.

Bug Fixes:

  • Ensure cached prompt tokens are billed correctly instead of being dropped or double-charged depending on provider reporting conventions.
  • Prevent real models (e.g., GLM via Anthropic-compatible gateways and dated/bprefixed IDs) from silently pricing at $0 due to exact-ID table mismatches.
  • Avoid reporting calls with missing token usage as priced-free by estimating counts locally and marking the resulting cost as approximate.
  • Guard against corrupt or unit-mismatched remote price feeds so they cannot overwrite the embedded table with implausible rates.

Enhancements:

  • Refine KnownModels, Lookup, and registration APIs to support overrides, unregistering, multi-layer resolution, and canonical/prefix-based lookups.
  • Deprecate legacy two-int pricing helpers while keeping them compatible by routing through the new tiered token computation.
  • Document the new cost accounting semantics, model ID normalization, and remote pricing behavior in the README.

Tests:

  • Add comprehensive tests for canonical model ID normalization, prefix resolution, tiered token pricing, cache-rate fallbacks, and reasoning token handling.
  • Add adapter usage tests that validate provider-specific token reporting normalization, estimation behavior, warnings on unreported usage, and GLM pricing over Anthropic gateways.
  • Add remote pricing and source-parsing tests to ensure feeds are parsed correctly, layering rules hold, disk caching works, outages are tolerated, and lookups never block on network I/O.

Summary by CodeRabbit

  • New Features

    • Added detailed token accounting for input, output, cached, cache-write, and reasoning tokens.
    • Added model-ID normalization and more accurate pricing across model variants.
    • Added optional remote pricing updates with validation, caching, fallback handling, and refresh status.
    • Added support for LiteLLM and OpenRouter pricing sources.
    • Added clear indicators when usage is reported by the provider or locally estimated.
  • Bug Fixes

    • Prevented cache tokens and reasoning tokens from being double-counted.
    • Improved handling of missing versus genuinely zero usage values.
    • Preserved pricing during remote-source outages or invalid updates.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @hallelx2, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds normalized token accounting, estimated-usage status fields, canonical model pricing, cache and reasoning charges, and opt-in remote price sources with validation and caching. Adapter and pricing tests cover provider-specific accounting, model variants, fallbacks, outages, and compatibility.

Changes

Pricing and usage accounting

Layer / File(s) Summary
Canonical model resolution and token billing
pricing/canonical.go, pricing/pricing.go, pricing/canonical_test.go
Model IDs are normalized before lookup. Pricing supports input, output, cache, and reasoning tokens. Legacy helpers delegate to ComputeTokens.
Remote pricing sources and snapshots
pricing/sources.go, pricing/remote.go, pricing/pricing.go
LiteLLM and OpenRouter feeds are parsed into validated price snapshots. Remote prices integrate with registered and embedded prices.
Remote pricing validation and resilience
pricing/remote.go, pricing/remote_test.go
Remote refreshes support source fallback, drift rejection, disk caching, callbacks, timeouts, and nonblocking lookups.
Provider usage normalization and estimation
internal/adapter/adapter.go, client.go, internal/adapter/adapter_usage_test.go, internal/adapter/adapter_tools_test.go
Provider token fields are normalized into disjoint categories. Missing usage is estimated and marked. Reported zero values remain distinct from absent values.
Public usage and pricing documentation
README.md
The README documents token accounting, reliability flags, model normalization, remote pricing, caching, and fallback behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant UseRemote
  participant PricingSource
  participant PricingSnapshot
  Application->>UseRemote: configure remote pricing
  UseRemote->>PricingSource: fetch pricing catalogue
  PricingSource-->>UseRemote: parsed model rates
  UseRemote->>PricingSnapshot: validate and install snapshot
  PricingSnapshot-->>Application: resolve model pricing
Loading

Possibly related PRs

  • hallelx2/llmgate#8: Both changes modify adapter usage handling, including reasoning-token extraction and duplicate token-accounting prevention.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's main changes: tiered cost accounting, model-ID normalization, and live pricing feeds.
Docstring Coverage ✅ Passed Docstring coverage is 86.96% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch halleluyaholudele/hal-529-pricing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements tiered, normalized cost accounting across providers, adds model-ID canonicalization for robust price lookup, and introduces an opt-in remote pricing layer with caching and validation, while updating the adapter and public API to expose richer Usage metadata and keeping legacy pricing APIs working.

Sequence diagram for normalized usage and tiered cost computation

sequenceDiagram
  actor User
  participant Adapter as Adapter.Complete
  participant Pricing as pricing

  User->>Adapter: Complete(ctx, req)
  Adapter->>Adapter: usage(ctx, model, folded, req, content)
  Adapter->>Adapter: extractTokens(f.genInfo)
  alt tokens missing
    Adapter->>Adapter: estimateTokens(ctx, req, content)
    Adapter->>Adapter: warnUnreported(provider, model)
  end
  Adapter->>Pricing: ComputeTokens(model, Tokens)
  Pricing->>Pricing: Lookup(model)
  Pricing->>Pricing: Canonical(model)
  Pricing->>Pricing: longestPrefix(table, canon)
  Pricing->>Pricing: cacheRates(model, Price)
  Pricing->>Pricing: familyCacheMultipliers(model)
  Pricing-->>Adapter: cost, priced
  Adapter-->>User: Response{Usage, CostUSD, Priced}
Loading

File-Level Changes

Change Details Files
Introduce tiered pricing and canonicalized model lookup with optional remote price snapshots in the pricing package.
  • Extend Price with cache and reasoning rates and populate default tables with provider-specific cache pricing.
  • Add Tokens struct and ComputeTokens, with family-aware cache rate fallbacks and reasoning token handling, deprecating but preserving Compute/ComputeWithOK semantics.
  • Implement Canonical model-ID normalization, longest-prefix matching, and family classification, and rework Lookup/Register/Unregister/KnownModels to support overrides and remote snapshots.
  • Add remote pricing layer via UseRemote/AsOf, with pluggable Sources, periodic refresh, disk cache, and snapshot vetting against embedded prices.
  • Provide concrete LiteLLM and OpenRouter Sources, per-token to per-Mtok conversion, body-size limits, and robust JSON parsing and rate handling.
pricing/pricing.go
pricing/remote.go
pricing/sources.go
pricing/canonical.go
Normalize and enrich per-call token usage accounting in the adapter, including cache and reasoning tokens and estimation when providers omit usage.
  • Refactor Complete to delegate usage construction to a new usage helper that extracts, normalizes, and prices token breakdowns.
  • Implement extractTokens to unify provider-specific GenerationInfo into disjoint token tiers and handle cache-inclusive vs cache-exclusive prompt counts.
  • Add estimateTokens to approximate usage via tokenizers when providers report no counts, and track TokensReported and Estimated flags in Usage.
  • Introduce UnreportedUsageFunc and per-provider/model once-only warnings for calls with missing usage, plus numeric helper lookupInt for presence-aware parsing.
internal/adapter/adapter.go
Expand public Usage API and documentation to expose detailed billing tiers and remote pricing behavior.
  • Extend llmgate.Usage with cache-write/read and reasoning token fields, plus TokensReported and Estimated flags, updating comments to describe disjoint normalization and semantics.
  • Update README with a new Cost accounting section explaining token tiers, trust flags, model-ID normalization, and opt-in remote pricing via UseRemote and AsOf.
client.go
README.md
Add comprehensive tests for canonicalization, tiered pricing, remote sources, remote layering, and adapter usage normalization.
  • Add canonical_test and remote_test to cover Canonical behavior, prefix matching rules, GLM variants, tiered ComputeTokens billing, cache multipliers, reasoning handling, unpriced behavior, legacy API compatibility, remote source parsing, snapshot vetting, layering, outage behavior, disk cache, and non-blocking lookups.
  • Add adapter_usage_test to verify provider-specific token normalization, correct cache write/read handling, reasoning subset semantics, estimation path behavior, warning coalescing, and GLM pricing over Anthropic gateway.
  • Update adapter_tools_test to use ComputeTokens instead of ComputeWithOK for GLM pricing regression coverage.
pricing/canonical_test.go
pricing/remote_test.go
internal/adapter/adapter_usage_test.go
internal/adapter/adapter_tools_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@hallelx2
hallelx2 changed the base branch from halleluyaholudele/hal-524-adapter-correctness to main August 2, 2026 12:44
…feeds

The reported cost was wrong in four independent ways. All four are fixed
here because they compound: each one alone still leaves the dollar figure
untrustworthy.

1. Cache and reasoning tokens were dropped. langchaingo already surfaces
CacheCreationInputTokens, CacheReadInputTokens, PromptCachedTokens and
ReasoningTokens; llmgate read none of them. Anthropic reports cache
tokens *outside* input_tokens, so a cached prompt — writes bill at 1.25x
input — was charged nothing at all. OpenAI and Google fold cached tokens
*into* the prompt count, so those were charged at the full rate instead
of the 0.5x/0.25x they actually cost. Usage now carries CacheWrite,
CacheRead and Reasoning, normalized to a disjoint form so the fields can
be summed on any provider, and Price carries the matching rate tiers with
family-specific fallbacks.

2. Lookup was an exact map hit. Every ID a real API returns missed it and
priced at $0: dated snapshots (claude-sonnet-4-5-20250929), SDK prefixes
(models/gemini-2.5-flash), Bedrock ARNs, and the gateway-qualified forms
the aggregate feeds use for the model we actually run (z-ai/glm-4.6).
Canonical strips prefixes, dates and revisions, then falls back to a
longest-prefix match — longest, because claude-sonnet-4 also prefixes
claude-sonnet-4-5-preview and the shorter key would misprice it.

3. Missing usage was reported as a priced $0 call. When GenerationInfo
carried no counts, ComputeWithOK(model, 0, 0) returned (0, true) —
Priced:true asserting a call that returned content was free. Usage gains
TokensReported and Estimated, and the adapter now estimates from the
tokenizer rather than reporting a zero.

4. The table was hand-maintained and drifted silently. UseRemote layers a
refreshed snapshot from LiteLLM or OpenRouter over the embedded defaults,
under any Register overrides. It is opt-in — importing the package does
no network I/O — and fails open throughout: lookups never touch the
network, a failed fetch keeps the last snapshot, a snapshot is persisted
so restarts start warm, and one whose rates have moved more than 10x from
the embedded values is rejected as a units error rather than adopted.

Rates are keyed by model ID alone, never provider+model: vectorless runs
GLM-4.6 through z.ai's Anthropic-compatible gateway, and a
provider-scoped key would look up "anthropic/glm-4.6" and miss.

BREAKING CHANGE: pricing.Compute and pricing.ComputeWithOK are deprecated
in favour of ComputeTokens, which takes a Tokens breakdown. Both still
work and are behaviour-compatible for callers with no cached tokens.
…ependent

TestRegisterBeatsNormalization left a global override in place, so with
-shuffle=on it could run before TestLookupResolvesVariants and make that
test see the pinned rate instead of the real one. Overrides are
process-global; every test that sets one has to undo it.
@hallelx2
hallelx2 force-pushed the halleluyaholudele/hal-529-pricing branch from 29eb794 to 73000c0 Compare August 2, 2026 12:46
@hallelx2
hallelx2 merged commit af5a9ec into main Aug 2, 2026
6 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
pricing/remote_test.go (1)

69-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compare converted rates with a tolerance, not ==.

Lines 69, 72, 95, and 98 compare float64 results of perMTok with ==. The fixture values are decimal fractions that float64 cannot represent exactly, so 0.0000022 * 1_000_000 == 2.2 depends on rounding landing exactly right. pricing/canonical_test.go already defines an approx helper with a 1e-9 tolerance for this reason. Reuse that pattern here. Both test files are in package pricing_test, so approx is already visible.

The same applies to Lines 129, 134, 180, and 279.

♻️ Example for the LiteLLM assertions
-	if sonnet.InputPerMTok != 3.0 || sonnet.OutputPerMTok != 15.0 {
-		t.Errorf("sonnet rates = %+v, want 3.00/15.00 per Mtok", sonnet)
-	}
-	if sonnet.CacheWritePerMTok != 3.75 || sonnet.CacheReadPerMTok != 0.30 {
-		t.Errorf("sonnet cache rates = %+v, want 3.75/0.30", sonnet)
-	}
+	approx(t, sonnet.InputPerMTok, 3.0, "sonnet input per Mtok")
+	approx(t, sonnet.OutputPerMTok, 15.0, "sonnet output per Mtok")
+	approx(t, sonnet.CacheWritePerMTok, 3.75, "sonnet cache-write per Mtok")
+	approx(t, sonnet.CacheReadPerMTok, 0.30, "sonnet cache-read per Mtok")

Also applies to: 95-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote_test.go` around lines 69 - 74, Update the float64 rate
assertions in the remote pricing tests, including the checks around sonnet and
the additional assertions at the referenced locations, to use the existing
pricing_test approx helper with its 1e-9 tolerance instead of direct equality.
Keep the expected rate values and failure messages unchanged, and apply the same
comparison pattern to all listed perMTok and cache-rate checks.
pricing/remote.go (1)

133-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Document that UseRemote blocks on the first fetch.

Line 133 runs refresh() synchronously. With the default two sources and the default 30s timeout, UseRemote can block the caller for about 60 seconds when both sources hang. The doc comment at Lines 67-78 does not state this. Either document the blocking first fetch, or run it in the background and let lookups use the embedded table until it lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote.go` at line 133, Document in the UseRemote comment that its
initial refresh() runs synchronously and may block the caller while the
configured sources time out; preserve the current blocking behavior unless
changing refresh initialization to run asynchronously with lookups falling back
to the embedded table.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/adapter/adapter_usage_test.go`:
- Around line 184-199: Update TestUnreportedUsageWarnsOncePerModel to remove the
anthropic/warn-once-test-model entry from unreportedSeen both before the test
assertion and during t.Cleanup, while retaining the existing UnreportedUsageFunc
reset.

In `@internal/adapter/adapter.go`:
- Around line 177-182: Update extractTokens to use a provider-specific rule
instead of comparing tk.Input and tk.CacheRead magnitudes. Pass a.provider into
extractTokens or identify the provider metadata keys, and subtract cache reads
only for providers whose prompt count includes them; preserve Anthropic’s
disjoint InputTokens and CacheReadInputTokens values. Add an Anthropic
regression case where InputTokens exceeds CacheReadInputTokens and verify totals
and charging remain correct.

In `@pricing/canonical_test.go`:
- Around line 87-88: Add test cleanup for every pricing override: in
pricing/canonical_test.go lines 87-88, register cleanup to unregister both
prefixtest-model-3 and prefixtest-model-3-5; in lines 211-213, add cleanup
inside the loop to unregister tc.model; and in lines 234-236, unregister
reasoning-rate-test via t.Cleanup.

In `@pricing/pricing.go`:
- Around line 144-153: Update the documentation for Register to remove the claim
that registered IDs match exactly before normalization. Describe that override
lookups use Canonical and longest-prefix matching, so a registered key can also
match dated, prefixed, and longer-suffixed model variants; leave the
implementation unchanged.

In `@pricing/remote.go`:
- Around line 260-276: Update loadCache to run the loaded cf.Prices through vet
before returning the cached snapshot. Preserve the existing empty and cacheTTL
checks, use vet’s validated prices in the returned snapshot, and rely on vet’s
idempotent behavior for the already-canonical keys.
- Around line 144-167: Make each UseRemote installation ownership-aware by
adding an owner token to snapshot and routing all snapshot writes through store,
including the initial install and ticker refresh. Update both stop functions to
use clear, which removes the global remote value only when its ownership token
still matches, and ensure store refuses writes after that installation is closed
so an in-flight refresh cannot resurrect it.

In `@pricing/sources.go`:
- Around line 76-84: Update the LiteLLM pricing filter around CacheCreationCost
and CacheReadCost to reject or clamp negative cache costs before constructing
the Price entry, matching the existing non-negative validation used by
OpenRouter’s parseRate. Preserve valid cache costs and the existing input/output
cost checks.

In `@README.md`:
- Around line 164-169: Update the fenced model-ID mapping block in README.md by
specifying the text language in its opening fence, changing it to a text-labeled
fence while preserving the mapping contents.
- Around line 181-186: Update the remote-pricing example around
pricing.UseRemote to be self-contained: add the required pricing and context
references in the example’s imports, replace the undefined ctx with
context.Background(), and handle the returned error directly without relying on
an undefined log symbol. Preserve the existing deferred stop behavior after
successful initialization.

---

Nitpick comments:
In `@pricing/remote_test.go`:
- Around line 69-74: Update the float64 rate assertions in the remote pricing
tests, including the checks around sonnet and the additional assertions at the
referenced locations, to use the existing pricing_test approx helper with its
1e-9 tolerance instead of direct equality. Keep the expected rate values and
failure messages unchanged, and apply the same comparison pattern to all listed
perMTok and cache-rate checks.

In `@pricing/remote.go`:
- Line 133: Document in the UseRemote comment that its initial refresh() runs
synchronously and may block the caller while the configured sources time out;
preserve the current blocking behavior unless changing refresh initialization to
run asynchronously with lookups falling back to the embedded table.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a11244e-79e2-4551-9dee-0b67a3cdc785

📥 Commits

Reviewing files that changed from the base of the PR and between 0382140 and 73000c0.

📒 Files selected for processing (11)
  • README.md
  • client.go
  • internal/adapter/adapter.go
  • internal/adapter/adapter_tools_test.go
  • internal/adapter/adapter_usage_test.go
  • pricing/canonical.go
  • pricing/canonical_test.go
  • pricing/pricing.go
  • pricing/remote.go
  • pricing/remote_test.go
  • pricing/sources.go

Comment on lines +184 to +199
func TestUnreportedUsageWarnsOncePerModel(t *testing.T) {
var seen []string
UnreportedUsageFunc = func(p llmgate.Provider, model string) {
seen = append(seen, string(p)+"/"+model)
}
t.Cleanup(func() { UnreportedUsageFunc = nil })

const model = "warn-once-test-model"
for range 3 {
usageFor(t, model, llmgate.ProviderAnthropic, map[string]any{"nothing": true})
}

if len(seen) != 1 {
t.Fatalf("warned %d times %v, want exactly once per provider/model", len(seen), seen)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reset the deduplication state in test cleanup.

Line 189 resets UnreportedUsageFunc but leaves anthropic/warn-once-test-model in unreportedSeen. A repeated run such as go test -count=2 can then receive no callback and fail the expectation. Delete this key before the assertion and in cleanup.

Proposed fix
 func TestUnreportedUsageWarnsOncePerModel(t *testing.T) {
+ const model = "warn-once-test-model"
+ key := string(llmgate.ProviderAnthropic) + "/" + model
+ unreportedSeen.Delete(key)
+
  var seen []string
  UnreportedUsageFunc = func(p llmgate.Provider, model string) {
    seen = append(seen, string(p)+"/"+model)
  }
- t.Cleanup(func() { UnreportedUsageFunc = nil })
-
- const model = "warn-once-test-model"
+ t.Cleanup(func() {
+   UnreportedUsageFunc = nil
+   unreportedSeen.Delete(key)
+ })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestUnreportedUsageWarnsOncePerModel(t *testing.T) {
var seen []string
UnreportedUsageFunc = func(p llmgate.Provider, model string) {
seen = append(seen, string(p)+"/"+model)
}
t.Cleanup(func() { UnreportedUsageFunc = nil })
const model = "warn-once-test-model"
for range 3 {
usageFor(t, model, llmgate.ProviderAnthropic, map[string]any{"nothing": true})
}
if len(seen) != 1 {
t.Fatalf("warned %d times %v, want exactly once per provider/model", len(seen), seen)
}
}
func TestUnreportedUsageWarnsOncePerModel(t *testing.T) {
const model = "warn-once-test-model"
key := string(llmgate.ProviderAnthropic) + "/" + model
unreportedSeen.Delete(key)
var seen []string
UnreportedUsageFunc = func(p llmgate.Provider, model string) {
seen = append(seen, string(p)+"/"+model)
}
t.Cleanup(func() {
UnreportedUsageFunc = nil
unreportedSeen.Delete(key)
})
for range 3 {
usageFor(t, model, llmgate.ProviderAnthropic, map[string]any{"nothing": true})
}
if len(seen) != 1 {
t.Fatalf("warned %d times %v, want exactly once per provider/model", len(seen), seen)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/adapter/adapter_usage_test.go` around lines 184 - 199, Update
TestUnreportedUsageWarnsOncePerModel to remove the
anthropic/warn-once-test-model entry from unreportedSeen both before the test
assertion and during t.Cleanup, while retaining the existing UnreportedUsageFunc
reset.

Comment on lines +177 to +182
// OpenAI and Google fold cache reads into the prompt count, Anthropic
// does not. Detect which by asking whether subtracting would go
// negative — that only happens when the counts were already disjoint.
if tk.CacheRead > 0 && tk.Input >= tk.CacheRead {
tk.Input -= tk.CacheRead
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not infer Anthropic token semantics from token magnitudes.

Line 180 subtracts cache reads when Input >= CacheRead. Anthropic reports these fields as disjoint. A response with 10,000 uncached input tokens and 8,000 cache-read tokens becomes 2,000 uncached input tokens. This undercounts TotalTokens by 8,000 and undercharges the request.

Pass a.provider into extractTokens, or identify the provider-specific metadata keys, and subtract cache reads only for providers that include them in prompt tokens. Add an Anthropic regression case where InputTokens > CacheReadInputTokens.

Proposed fix
- tk, reported := extractTokens(f.genInfo)
+ tk, reported := extractTokens(a.provider, f.genInfo)

-func extractTokens(gi map[string]any) (pricing.Tokens, bool) {
+func extractTokens(provider llmgate.Provider, gi map[string]any) (pricing.Tokens, bool) {
  ...
- if tk.CacheRead > 0 && tk.Input >= tk.CacheRead {
+ if provider != llmgate.ProviderAnthropic && tk.CacheRead > 0 && tk.Input >= tk.CacheRead {
    tk.Input -= tk.CacheRead
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// OpenAI and Google fold cache reads into the prompt count, Anthropic
// does not. Detect which by asking whether subtracting would go
// negative — that only happens when the counts were already disjoint.
if tk.CacheRead > 0 && tk.Input >= tk.CacheRead {
tk.Input -= tk.CacheRead
}
// OpenAI and Google fold cache reads into the prompt count, Anthropic
// does not. Detect which by asking whether subtracting would go
// negative — that only happens when the counts were already disjoint.
if provider != llmgate.ProviderAnthropic && tk.CacheRead > 0 && tk.Input >= tk.CacheRead {
tk.Input -= tk.CacheRead
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/adapter/adapter.go` around lines 177 - 182, Update extractTokens to
use a provider-specific rule instead of comparing tk.Input and tk.CacheRead
magnitudes. Pass a.provider into extractTokens or identify the provider metadata
keys, and subtract cache reads only for providers whose prompt count includes
them; preserve Anthropic’s disjoint InputTokens and CacheReadInputTokens values.
Add an Anthropic regression case where InputTokens exceeds CacheReadInputTokens
and verify totals and charging remain correct.

Comment thread pricing/canonical_test.go
Comment on lines +87 to +88
pricing.Register("prefixtest-model-3", cheap)
pricing.Register("prefixtest-model-3-5", dear)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Three tests leak process-global pricing overrides. pricing.Register writes to the package-level overrides map, and that map outlives the test that wrote it. Every leaked key stays in the price book for the rest of the test binary and participates in the longestPrefix scan of later lookups. The PR already fixed this at Line 125 and in pricing/remote_test.go Line 142; these three sites were missed.

  • pricing/canonical_test.go#L87-L88: add a t.Cleanup that calls pricing.Unregister for prefixtest-model-3 and prefixtest-model-3-5.
  • pricing/canonical_test.go#L211-L213: add a t.Cleanup inside the loop that calls pricing.Unregister(tc.model).
  • pricing/canonical_test.go#L234-L236: add a t.Cleanup that calls pricing.Unregister("reasoning-rate-test").
📍 Affects 1 file
  • pricing/canonical_test.go#L87-L88 (this comment)
  • pricing/canonical_test.go#L211-L213
  • pricing/canonical_test.go#L234-L236
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/canonical_test.go` around lines 87 - 88, Add test cleanup for every
pricing override: in pricing/canonical_test.go lines 87-88, register cleanup to
unregister both prefixtest-model-3 and prefixtest-model-3-5; in lines 211-213,
add cleanup inside the loop to unregister tc.model; and in lines 234-236,
unregister reasoning-rate-test via t.Cleanup.

Comment thread pricing/pricing.go
Comment on lines 144 to +153
// Register overrides or adds a price. Safe for init() in callers.
//
// Registered IDs match exactly, before any normalization, and sit above
// both the remote snapshot and the embedded table — so this is how to pin
// a rate that would otherwise resolve elsewhere.
func Register(model string, p Price) {
priceMu.Lock()
defer priceMu.Unlock()
prices[model] = p
overrides[model] = p
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the Register doc: overrides are not matched exactly.

The comment states that registered IDs "match exactly, before any normalization". Lookup passes overrides to lookupIn at Line 108, and lookupIn applies Canonical and longestPrefix to that table as well. A registered key therefore also captures dated, prefixed, and longer-suffixed variants. Register("claude-sonnet-4-5", ...) also prices claude-sonnet-4-5-20250929 and claude-sonnet-4-5-preview.

Either update the doc to describe the three-step match, or make the overrides layer exact-match only if that was the intent.

📝 Proposed doc correction
-// Register overrides or adds a price. Safe for init() in callers.
-//
-// Registered IDs match exactly, before any normalization, and sit above
-// both the remote snapshot and the embedded table — so this is how to pin
-// a rate that would otherwise resolve elsewhere.
+// Register overrides or adds a price. Safe for init() in callers.
+//
+// The overrides layer sits above both the remote snapshot and the
+// embedded table, and it resolves like every other layer: exact ID
+// first, then the canonical form, then the longest matching prefix. A
+// registered base ID therefore also pins its dated and suffixed
+// variants.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register overrides or adds a price. Safe for init() in callers.
//
// Registered IDs match exactly, before any normalization, and sit above
// both the remote snapshot and the embedded table — so this is how to pin
// a rate that would otherwise resolve elsewhere.
func Register(model string, p Price) {
priceMu.Lock()
defer priceMu.Unlock()
prices[model] = p
overrides[model] = p
}
// Register overrides or adds a price. Safe for init() in callers.
//
// The overrides layer sits above both the remote snapshot and the
// embedded table, and it resolves like every other layer: exact ID
// first, then the canonical form, then the longest matching prefix. A
// registered base ID therefore also pins its dated and suffixed
// variants.
func Register(model string, p Price) {
priceMu.Lock()
defer priceMu.Unlock()
overrides[model] = p
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/pricing.go` around lines 144 - 153, Update the documentation for
Register to remove the claim that registered IDs match exactly before
normalization. Describe that override lookups use Canonical and longest-prefix
matching, so a registered key can also match dated, prefixed, and
longer-suffixed model variants; leave the implementation unchanged.

Comment thread pricing/remote.go
Comment on lines +144 to +167
done := make(chan struct{})
go func() {
t := time.NewTicker(interval)
defer t.Stop()
for {
select {
case <-t.C:
refresh()
case <-done:
return
case <-ctx.Done():
return
}
}
}()

var once sync.Once
return func() {
once.Do(func() {
close(done)
remote.Store(nil)
})
}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

stop() can be undone by an in-flight refresh, and it clears a snapshot it may not own.

remote is a package-level pointer shared by every UseRemote call. Two problems follow.

A refresh that is already running when stop() executes still reaches Line 120 and stores its snapshot. Line 163 closes done, but that only prevents the next tick. The layer that stop() removed at Line 164 therefore reappears, which contradicts the guarantee documented at Lines 76-78.

A second UseRemote call installs into the same global pointer. If the first caller stops afterwards, Line 164 clears the second caller's snapshot.

Give each installation an ownership token and clear the pointer only when the installed snapshot still belongs to that installation. Also stop storing once the installation is closed.

🔒 Sketch of an ownership-aware stop
+	// Each installation owns the snapshots it stores, so a stop never
+	// clears a layer another caller installed and a late refresh never
+	// resurrects a stopped one.
+	type owner struct{}
+	var (
+		ownMu   sync.Mutex
+		stopped bool
+	)
+	store := func(snap *snapshot) {
+		ownMu.Lock()
+		defer ownMu.Unlock()
+		if stopped {
+			return
+		}
+		snap.owner = &ownMu
+		remote.Store(snap)
+	}
+	clear := func() {
+		ownMu.Lock()
+		defer ownMu.Unlock()
+		stopped = true
+		if cur := remote.Load(); cur != nil && cur.owner == &ownMu {
+			remote.Store(nil)
+		}
+	}

This needs a matching owner any field on snapshot. Use store at Lines 95 and 120, and clear in both stop functions at Lines 137 and 164.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote.go` around lines 144 - 167, Make each UseRemote installation
ownership-aware by adding an owner token to snapshot and routing all snapshot
writes through store, including the initial install and ticker refresh. Update
both stop functions to use clear, which removes the global remote value only
when its ownership token still matches, and ensure store refuses writes after
that installation is closed so an in-flight refresh cannot resurrect it.

Comment thread pricing/remote.go
Comment on lines +260 to +276
func loadCache(dir string) (*snapshot, error) {
b, err := os.ReadFile(filepath.Join(dir, cacheName))
if err != nil {
return nil, err
}
var cf cacheFile
if err := json.Unmarshal(b, &cf); err != nil {
return nil, err
}
if len(cf.Prices) == 0 {
return nil, fmt.Errorf("cached snapshot is empty")
}
if time.Since(cf.AsOf) > cacheTTL {
return nil, fmt.Errorf("cached snapshot from %s is too old", cf.AsOf.Format(time.RFC3339))
}
return &snapshot{prices: cf.Prices, asOf: cf.AsOf, source: cf.Source + " (cached)"}, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the cached snapshot with vet before installing it.

loadCache checks only that the file parses, is non-empty, and is within cacheTTL. It skips vet, so the disk path bypasses the non-positive-rate filter at Lines 200-202 and the drift check at Lines 227-232. A cache written by an earlier build, or an edited file on disk, is adopted at startup without any sanity check. UseRemote installs it directly at Line 95.

Run the loaded prices through vet. The keys are already canonical, and vet is idempotent on canonical keys.

🛡️ Proposed fix
 	if time.Since(cf.AsOf) > cacheTTL {
 		return nil, fmt.Errorf("cached snapshot from %s is too old", cf.AsOf.Format(time.RFC3339))
 	}
-	return &snapshot{prices: cf.Prices, asOf: cf.AsOf, source: cf.Source + " (cached)"}, nil
+	// A cached file is data from a previous process. Re-validate it
+	// against the current embedded table rather than trusting it.
+	cleaned, err := vet(cf.Prices)
+	if err != nil {
+		return nil, fmt.Errorf("cached snapshot rejected: %w", err)
+	}
+	return &snapshot{prices: cleaned, asOf: cf.AsOf, source: cf.Source + " (cached)"}, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func loadCache(dir string) (*snapshot, error) {
b, err := os.ReadFile(filepath.Join(dir, cacheName))
if err != nil {
return nil, err
}
var cf cacheFile
if err := json.Unmarshal(b, &cf); err != nil {
return nil, err
}
if len(cf.Prices) == 0 {
return nil, fmt.Errorf("cached snapshot is empty")
}
if time.Since(cf.AsOf) > cacheTTL {
return nil, fmt.Errorf("cached snapshot from %s is too old", cf.AsOf.Format(time.RFC3339))
}
return &snapshot{prices: cf.Prices, asOf: cf.AsOf, source: cf.Source + " (cached)"}, nil
}
func loadCache(dir string) (*snapshot, error) {
b, err := os.ReadFile(filepath.Join(dir, cacheName))
if err != nil {
return nil, err
}
var cf cacheFile
if err := json.Unmarshal(b, &cf); err != nil {
return nil, err
}
if len(cf.Prices) == 0 {
return nil, fmt.Errorf("cached snapshot is empty")
}
if time.Since(cf.AsOf) > cacheTTL {
return nil, fmt.Errorf("cached snapshot from %s is too old", cf.AsOf.Format(time.RFC3339))
}
// A cached file is data from a previous process. Re-validate it
// against the current embedded table rather than trusting it.
cleaned, err := vet(cf.Prices)
if err != nil {
return nil, fmt.Errorf("cached snapshot rejected: %w", err)
}
return &snapshot{prices: cleaned, asOf: cf.AsOf, source: cf.Source + " (cached)"}, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/remote.go` around lines 260 - 276, Update loadCache to run the loaded
cf.Prices through vet before returning the cached snapshot. Preserve the
existing empty and cacheTTL checks, use vet’s validated prices in the returned
snapshot, and rely on vet’s idempotent behavior for the already-canonical keys.

Comment thread pricing/sources.go
Comment on lines +76 to +84
if e.InputCostPerToken <= 0 || e.OutputCostPerToken <= 0 {
continue
}
out[id] = Price{
InputPerMTok: perMTok(e.InputCostPerToken),
OutputPerMTok: perMTok(e.OutputCostPerToken),
CacheWritePerMTok: perMTok(e.CacheCreationCost),
CacheReadPerMTok: perMTok(e.CacheReadCost),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clamp negative LiteLLM cache costs.

Lines 82 and 83 convert CacheCreationCost and CacheReadCost without a sign check. vet in pricing/remote.go Lines 200-202 validates only InputPerMTok and OutputPerMTok, so a negative cache cost reaches the snapshot and lowers the computed bill. The OpenRouter path already rejects negatives in parseRate at Line 166. Apply the same guard here.

🛡️ Proposed guard
+		cacheWrite, cacheRead := e.CacheCreationCost, e.CacheReadCost
+		if cacheWrite < 0 {
+			cacheWrite = 0
+		}
+		if cacheRead < 0 {
+			cacheRead = 0
+		}
 		out[id] = Price{
 			InputPerMTok:      perMTok(e.InputCostPerToken),
 			OutputPerMTok:     perMTok(e.OutputCostPerToken),
-			CacheWritePerMTok: perMTok(e.CacheCreationCost),
-			CacheReadPerMTok:  perMTok(e.CacheReadCost),
+			CacheWritePerMTok: perMTok(cacheWrite),
+			CacheReadPerMTok:  perMTok(cacheRead),
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if e.InputCostPerToken <= 0 || e.OutputCostPerToken <= 0 {
continue
}
out[id] = Price{
InputPerMTok: perMTok(e.InputCostPerToken),
OutputPerMTok: perMTok(e.OutputCostPerToken),
CacheWritePerMTok: perMTok(e.CacheCreationCost),
CacheReadPerMTok: perMTok(e.CacheReadCost),
}
if e.InputCostPerToken <= 0 || e.OutputCostPerToken <= 0 {
continue
}
cacheWrite, cacheRead := e.CacheCreationCost, e.CacheReadCost
if cacheWrite < 0 {
cacheWrite = 0
}
if cacheRead < 0 {
cacheRead = 0
}
out[id] = Price{
InputPerMTok: perMTok(e.InputCostPerToken),
OutputPerMTok: perMTok(e.OutputCostPerToken),
CacheWritePerMTok: perMTok(cacheWrite),
CacheReadPerMTok: perMTok(cacheRead),
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pricing/sources.go` around lines 76 - 84, Update the LiteLLM pricing filter
around CacheCreationCost and CacheReadCost to reject or clamp negative cache
costs before constructing the Price entry, matching the existing non-negative
validation used by OpenRouter’s parseRate. Preserve valid cache costs and the
existing input/output cost checks.

Comment thread README.md
Comment on lines +164 to +169
```
claude-sonnet-4-5-20250929 -> claude-sonnet-4-5
models/gemini-2.5-flash -> gemini-2.5-flash
us.anthropic.claude-opus-4-1-v1:0 -> claude-opus-4-1
z-ai/glm-4.6 -> glm-4.6
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify a language for the fenced block.

markdownlint reports MD040 at Line 164. Add text to this model-ID mapping block.

-```
+```text
 claude-sonnet-4-5-20250929        -> claude-sonnet-4-5
 ...

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.23.1)</summary>

[warning] 164-164: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @README.md around lines 164 - 169, Update the fenced model-ID mapping block
in README.md by specifying the text language in its opening fence, changing it
to a text-labeled fence while preserving the mapping contents.


</details>

<!-- fingerprinting:phantom:poseidon:terra -->

<!-- cr-indicator-types:potential_issue -->

<!-- cr-comment:v1:afd613c609ef0de8b95133b0 -->

_Source: Linters/SAST tools_

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread README.md
Comment on lines +181 to +186
```go
stop, err := pricing.UseRemote(ctx, pricing.RemoteConfig{
CacheDir: "/var/cache/llmgate", // survive restarts
OnError: func(src string, err error) { log.Warn("price refresh", "src", src, "err", err) },
})
defer stop()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the remote-pricing example self-contained.

The sample uses undefined ctx and log, and its surrounding import list does not include pricing. Use context.Background() and handle the error directly, or show the required logger and imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 181 - 186, Update the remote-pricing example around
pricing.UseRemote to be self-contained: add the required pricing and context
references in the example’s imports, replace the undefined ctx with
context.Background(), and handle the returned error directly without relying on
an undefined log symbol. Preserve the existing deferred stop behavior after
successful initialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant