Skip to content

fix(billing): split image input tokens for gpt-image-2 - #2176

Merged
steebchen merged 8 commits into
mainfrom
bill-gpt-image-2-input-tokens
May 7, 2026
Merged

steebchen merged 8 commits into
mainfrom
bill-gpt-image-2-input-tokens

Conversation

@steebchen

@steebchen steebchen commented May 6, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Bill input_tokens_details.image_tokens from OpenAI/Azure gpt-image-2 responses against a new imageInputPrice of $8/M, and correct inputPrice from $8/M to $5/M (the actual text-input rate). Together this stops billing image input tokens at the text rate and aligns gateway pricing with OpenAI's published rates.
  • Plumb the upstream-reported imageInputTokens / imageOutputTokens from the parsed response through calculateCosts, and prefer those over the legacy inputImageCount * 560 estimate when the model has output: \[\"image\"].
  • For OpenAI/Azure/xAI image-output models, subtract image tokens from prompt tokens before applying inputPrice so the image portion isn't double-billed at both the text and image rates.

gpt-image-1 isn't defined in this repo's model catalog (only gpt-image-2), so there's nothing to change there.

Verified pricing (per 1M tokens)

text input image input cached input image output
gpt-image-2 (OpenAI) $5 $8 $2 $30
gpt-image-2 (Azure) $5 $8 $2 $30 (with 20% discount)

Sources: OpenAI developer docs pricing table, the existing research note in `docs/providers/openai/gpt-image-2/openai-gpt-image-2.md`.

Why scoped to image-output models

isImageOutputModel = modelInfo.output?.includes(\"image\") gates the new "use reported tokens" path. Without this gate, gpt-4o's legacy per-image imageInputPrice: 0.00553 (which is encoded as a flat per-image fee, not per-token) would get applied to thousands of vision tokens and over-bill catastrophically. That field is dormant today (because inputImageCount only fires for Gemini image-preview models) and a separate cleanup PR can remove it.

Test plan

  • apps/gateway/src/lib/costs.spec.ts — 27/27 pass, including two new tests covering OpenAI and Azure gpt-image-2 image-edit billing
  • apps/gateway/src/{lib,chat} — 518/518 pass
  • packages/actions — 94/94 pass
  • pnpm build:core clean
  • E2E apps/gateway/src/images.e2e.ts (gpt-image-2 edits) when run with live OpenAI/Azure credentials

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Separate tracking/billing for image input and output tokens; accepts upstream-reported image token counts.
    • Updated GPT-Image-2 pricing support with distinct image-input pricing and provider-specific adjustments; added cached image-input pricing option.
  • Bug Fixes

    • Prevents double-billing of image tokens by splitting prompt billing when image tokens are present.
  • Tests

    • Added/updated tests for GPT-Image-2 image billing split and provider discount behavior.

Bill `input_tokens_details.image_tokens` from OpenAI/Azure
gpt-image-2 responses against `imageInputPrice` ($8/M) and the text
remainder against `inputPrice` ($5/M, corrected from $8/M). Plumb the
upstream-reported image_tokens / output image_tokens counts through
`calculateCosts` and prefer them over the count*estimate fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 6, 2026 19:11

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59c4efbaea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 101 to 102
webSearchCount: number | null = null,
organizationId: string | null = null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass image token details when billing cached responses

Because the new split pricing depends on reportedImageInputTokens/reportedImageOutputTokens, cached non-streaming responses still get billed incorrectly: the cache-hit call at apps/gateway/src/chat/chat.ts:3716 recalculates from cachedResponse.usage but never passes prompt_tokens_details.image_tokens or completion_tokens_details.image_tokens (and uses outputImageCount = 0). For cached gpt-image-2 edits/generations, this means image input tokens fall back to the new $5/M text rate and image output tokens are not charged at the $30/M image rate, even though those details are stored in the transformed cached response.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented May 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds optional upstream-reported image input/output token parameters to calculateCosts, prefers those values over estimates, splits GPT-Image-2 pricing between text and image input rates, updates chat call sites to pass new args, and adds tests for split billing and Azure discount behavior.

Changes

Image Token Reporting & Billing

Layer / File(s) Summary
Data Shape
packages/models/src/models/openai.ts, packages/models/src/models.ts
Add imageInputPrice and cachedImageInputPrice?; update gpt-image-2 provider entries (text inputPrice -> 5/1e6, outputPrice -> 0, add imageInputPrice).
Core Implementation
apps/gateway/src/lib/costs.ts
Extend calculateCosts with reportedImageInputTokens and reportedImageOutputTokens; prefer reported counts, apportion cached prompt tokens to avoid double-billing, and select image output tokens via reported/estimate/total/legacy fallbacks.
Wiring
apps/gateway/src/chat/chat.ts
Streaming and non-streaming calculateCosts call sites updated to pass imageInputTokens and imageOutputTokens (currently null at these sites).
Tests
apps/gateway/src/lib/costs.spec.ts, packages/actions/src/models.spec.ts
Adjust gpt-image-2 input-rate expectation; add tests verifying reported-image split billing and Azure discount application; exclude providers with image pricing from zero-pricing free-flag assertion.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Gateway.Chat
  participant Gateway.Costs
  participant ModelsData
  Client->>Gateway.Chat: request (may include image reports)
  Gateway.Chat->>Gateway.Costs: calculateCosts(..., reportedImageInputTokens?, reportedImageOutputTokens?)
  Gateway.Costs->>ModelsData: lookup model prices (inputPrice, imageInputPrice, imageOutputPrice, discount)
  Gateway.Costs-->>Gateway.Chat: cost breakdown
  Gateway.Chat-->>Client: stream response + usage/cost chunk
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

🚥 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 and specifically describes the main change: splitting image input token billing for the gpt-image-2 model, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bill-gpt-image-2-input-tokens

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 and usage tips.

Copilot AI 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.

Pull request overview

This PR updates the gateway’s billing logic for OpenAI/Azure gpt-image-2 to correctly price image input tokens separately from text input tokens, aligning internal pricing with published provider rates and avoiding double-billing image tokens at the text rate.

Changes:

  • Updates gpt-image-2 provider mappings to set inputPrice to the text-input rate and introduce a new per-token imageInputPrice.
  • Extends calculateCosts to accept upstream-reported image token counts and uses them for image-output models; subtracts image input tokens from billable text prompt tokens for OpenAI/Azure/xAI.
  • Adds unit tests covering split image/text input billing (including Azure discount behavior).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/models/src/models/openai.ts Adjusts gpt-image-2 pricing and introduces imageInputPrice for OpenAI/Azure mappings.
apps/gateway/src/lib/costs.ts Uses upstream image token counts for image-output models and prevents double-billing by subtracting image tokens from text prompt billing.
apps/gateway/src/lib/costs.spec.ts Updates expectations and adds tests for split image/text input billing for gpt-image-2 (OpenAI + Azure).
apps/gateway/src/chat/chat.ts Plumbs parsed imageInputTokens / imageOutputTokens into calculateCosts for non-streaming responses.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 85 to 106
export async function calculateCosts(
model: string,
provider: string,
promptTokens: number | null,
completionTokens: number | null,
cachedTokens: number | null = null,
fullOutput?: {
messages?: ChatMessage[];
prompt?: string;
completion?: string;
toolResults?: ToolCall[];
},
reasoningTokens: number | null = null,
outputImageCount = 0,
imageSize?: string,
inputImageCount = 0,
webSearchCount: number | null = null,
organizationId: string | null = null,
imageQuality?: string,
reportedImageInputTokens: number | null = null,
reportedImageOutputTokens: number | null = null,
) {
Comment on lines +311 to +312
reportedImageInputTokens &&
reportedImageInputTokens > 0
Comment on lines 375 to +384
imageOutputTokens =
imageOutputTokensPerImage !== undefined
? outputImageCount * imageOutputTokensPerImage
: totalOutputTokens > 0
? totalOutputTokens
: outputImageCount * LEGACY_DEFAULT_TOKENS_PER_IMAGE;
isImageOutputModel &&
reportedImageOutputTokens &&
reportedImageOutputTokens > 0
? reportedImageOutputTokens
: imageOutputTokensPerImage !== undefined
? outputImageCount * imageOutputTokensPerImage
: totalOutputTokens > 0
? totalOutputTokens
: outputImageCount * LEGACY_DEFAULT_TOKENS_PER_IMAGE;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/gateway/src/chat/chat.ts (1)

9304-9324: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate reported image tokens through the cached billing path too.

This fixes the live non-streaming path, but the cached non-streaming branch still calls calculateCosts(...) without usage.prompt_tokens_details.image_tokens / usage.completion_tokens_details.image_tokens from the cached response. That means a cached gpt-image-2 response can be billed differently from the original uncached response, and image output can fall back to the wrong pricing path because Line 3724 still passes 0 for outputImageCount.

🤖 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 `@apps/gateway/src/chat/chat.ts` around lines 9304 - 9324, The cached
non-streaming billing path is calling calculateCosts(...) without propagating
image token/details from the cached response; update the cached-response branch
that builds the calculateCosts(...) call to extract
usage.prompt_tokens_details.image_tokens and
usage.completion_tokens_details.image_tokens (and cached output image count)
from the cached response and pass them into the calculateCosts parameters
(imageInputTokens, imageOutputTokens and outputImageCount) instead of hardcoding
0/omitting them so cached gpt-image-2 responses are billed the same as live
responses.
🧹 Nitpick comments (1)
apps/gateway/src/lib/costs.ts (1)

308-326: 💤 Low value

Defensive: clamp reportedImageInputTokens against uncachedPromptTokens.

If the upstream ever reports image_tokens larger than prompt_tokens - cached_tokens (e.g., transient provider bug, or cached tokens that overlap with image tokens), billableTextPromptTokens floors to 0 but imageInputCost = reportedImageInputTokens * imageInputPrice is still billed in full — over-billing the customer for tokens the provider never reported as input. A small clamp would make the gate symmetric with the Math.max(0, …) already used at line 343.

♻️ Suggested clamp
 	if (
 		imageInputPricePerToken &&
 		isImageOutputModel &&
 		reportedImageInputTokens &&
 		reportedImageInputTokens > 0
 	) {
-		imageInputTokens = reportedImageInputTokens;
+		const uncachedForImageCap = cachedTokens
+			? calculatedPromptTokens - cachedTokens
+			: calculatedPromptTokens;
+		imageInputTokens = Math.min(
+			reportedImageInputTokens,
+			Math.max(0, uncachedForImageCap),
+		);
 		imageInputCost = new Decimal(imageInputTokens)
 			.times(imageInputPricePerToken)
 			.times(discountMultiplier);
 	} else if (imageInputPricePerToken && inputImageCount > 0) {

Also applies to: 341-349

🤖 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 `@apps/gateway/src/lib/costs.ts` around lines 308 - 326, Clamp
reportedImageInputTokens to not exceed the available uncached prompt tokens
before using it for billing: replace direct use of reportedImageInputTokens when
computing imageInputTokens/imageInputCost with a bounded value like
clampedImageTokens = Math.min(reportedImageInputTokens, Math.max(0,
uncachedPromptTokens)) (or equivalent), then use clampedImageTokens in place of
reportedImageInputTokens; do the same defensive clamp wherever
reportedImageInputTokens is used (also update the similar block that handles the
legacy-tokens path).
🤖 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.

Outside diff comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 9304-9324: The cached non-streaming billing path is calling
calculateCosts(...) without propagating image token/details from the cached
response; update the cached-response branch that builds the calculateCosts(...)
call to extract usage.prompt_tokens_details.image_tokens and
usage.completion_tokens_details.image_tokens (and cached output image count)
from the cached response and pass them into the calculateCosts parameters
(imageInputTokens, imageOutputTokens and outputImageCount) instead of hardcoding
0/omitting them so cached gpt-image-2 responses are billed the same as live
responses.

---

Nitpick comments:
In `@apps/gateway/src/lib/costs.ts`:
- Around line 308-326: Clamp reportedImageInputTokens to not exceed the
available uncached prompt tokens before using it for billing: replace direct use
of reportedImageInputTokens when computing imageInputTokens/imageInputCost with
a bounded value like clampedImageTokens = Math.min(reportedImageInputTokens,
Math.max(0, uncachedPromptTokens)) (or equivalent), then use clampedImageTokens
in place of reportedImageInputTokens; do the same defensive clamp wherever
reportedImageInputTokens is used (also update the similar block that handles the
legacy-tokens path).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ddf0801f-428d-4c68-b6e5-97cad39cfa71

📥 Commits

Reviewing files that changed from the base of the PR and between e600a0b and 59c4efb.

📒 Files selected for processing (4)
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • packages/models/src/models/openai.ts

steebchen-bot and others added 2 commits May 7, 2026 13:03
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Resolved merge conflicts with origin/main:

Conflict 1 — costs.ts function signature: PR added reportedImageInputTokens and reportedImageOutputTokens params; main added options?: { cacheWriteTokens, cacheWrite1hTokens }. Merged both — image token params added before the options param.

Conflict 2 — costs.ts uncached prompt tokens logic: PR added promptIncludesImageTokens/billableTextPromptTokens to subtract image tokens from billable text for OpenAI/Azure/xAI; main added separatelyPricedCacheWriteTokens to exclude cache-write tokens. Merged both — cache-write exclusion applied first, then image-token exclusion on top.

Conflict 3 — chat.ts calculateCosts call: PR passed imageInputTokens, imageOutputTokens; main passed the { cacheWriteTokens, cacheWrite1hTokens } options object. Merged to pass all three.

Also fixed 5 other calculateCosts callers in chat.ts and 3 in costs.spec.ts that came from main and needed null, null added for the new image token positional params.

So the gpt-image-2 azure billing test still passes if the discount
value is changed later.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44729dea14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/gateway/src/lib/costs.ts Outdated
Comment on lines +355 to +357
imageInputCost = new Decimal(imageInputTokens)
.times(imageInputPricePerToken)
.times(discountMultiplier);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid charging cached image tokens at full input rate

When OpenAI/Azure returns input_tokens_details.cached_tokens together with image_tokens, cached input is subtracted from the text prompt path but imageInputCost still multiplies the full reported image-token count by the full image input rate; those cached image tokens are then also charged again via cachedInputCost. For repeated gpt-image-2 image edits where the cached tokens include image input tokens, this overcharges instead of applying the cached-input rate, so allocate/subtract cached tokens from the image-token portion before multiplying by imageInputPrice.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/gateway/src/lib/costs.spec.ts`:
- Around line 520-565: The Azure image test "should apply azure discount on top
of split image/text input pricing for gpt-image-2" is missing assertions for
imageOutputTokens, imageOutputCost, and totalCost; update the spec to assert
that result.imageOutputTokens equals reportedImageOutputTokens,
result.imageOutputCost is close to expectedImageOutput (use the same
discountMultiplier and per-token rate used to compute expectedImageOutputCost),
and result.totalCost equals the sum of inputCost and outputCost (or assert
closeTo expectedTextInputCost + expectedImageInputCost +
expectedImageOutputCost). Reference the test name, calculateCosts call, and
result properties imageOutputTokens, imageOutputCost, and totalCost when adding
these assertions.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5d84783c-cb76-43ed-b398-c69f90c23870

📥 Commits

Reviewing files that changed from the base of the PR and between 2d29c96 and 44729de.

📒 Files selected for processing (1)
  • apps/gateway/src/lib/costs.spec.ts

Comment on lines +520 to +565
it("should apply azure discount on top of split image/text input pricing for gpt-image-2", async () => {
const promptTokens = 524;
const reportedImageInputTokens = 512;
const completionTokens = 196;
const reportedImageOutputTokens = 196;

const result = await calculateCosts(
"gpt-image-2",
"azure",
promptTokens,
completionTokens,
null,
undefined,
null,
1,
"1024x1024",
0,
null,
null,
"low",
reportedImageInputTokens,
reportedImageOutputTokens,
);

// Read discount from the model definition so the test stays correct
// even if the azure discount value changes.
const azureProvider = models
.find((m) => m.id === "gpt-image-2")
?.providers.find((p) => p.providerId === "azure");
const discountMultiplier = 1 - (azureProvider?.discount ?? 0);
const expectedTextInputCost =
(promptTokens - reportedImageInputTokens) *
(5 / 1e6) *
discountMultiplier;
const expectedImageInputCost =
reportedImageInputTokens * (8 / 1e6) * discountMultiplier;
const expectedImageOutputCost =
reportedImageOutputTokens * (30 / 1e6) * discountMultiplier;

expect(result.imageInputTokens).toBe(reportedImageInputTokens);
expect(result.imageInputCost).toBeCloseTo(expectedImageInputCost);
expect(result.inputCost).toBeCloseTo(
expectedTextInputCost + expectedImageInputCost,
);
expect(result.outputCost).toBeCloseTo(expectedImageOutputCost);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Azure test is missing imageOutputTokens, imageOutputCost, and totalCost assertions.

The companion OpenAI test (lines 507–517) checks all six cost breakdown fields plus totalCost, but the Azure variant only checks imageInputTokens, imageInputCost, inputCost, and outputCost. This leaves the discount-applied imageOutputCost and the aggregate totalCost unverified — which is exactly what the PR description says this test exists to confirm ("the gpt-image-2 image output portion uses reportedImageOutputTokens for its discounted image-output cost").

🛡️ Proposed additions
 		expect(result.imageInputTokens).toBe(reportedImageInputTokens);
 		expect(result.imageInputCost).toBeCloseTo(expectedImageInputCost);
 		expect(result.inputCost).toBeCloseTo(
 			expectedTextInputCost + expectedImageInputCost,
 		);
 		expect(result.outputCost).toBeCloseTo(expectedImageOutputCost);
+		expect(result.imageOutputTokens).toBe(reportedImageOutputTokens);
+		expect(result.imageOutputCost).toBeCloseTo(expectedImageOutputCost);
+		expect(result.totalCost).toBeCloseTo(
+			expectedTextInputCost + expectedImageInputCost + expectedImageOutputCost,
+		);
 	});
🤖 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 `@apps/gateway/src/lib/costs.spec.ts` around lines 520 - 565, The Azure image
test "should apply azure discount on top of split image/text input pricing for
gpt-image-2" is missing assertions for imageOutputTokens, imageOutputCost, and
totalCost; update the spec to assert that result.imageOutputTokens equals
reportedImageOutputTokens, result.imageOutputCost is close to
expectedImageOutput (use the same discountMultiplier and per-token rate used to
compute expectedImageOutputCost), and result.totalCost equals the sum of
inputCost and outputCost (or assert closeTo expectedTextInputCost +
expectedImageInputCost + expectedImageOutputCost). Reference the test name,
calculateCosts call, and result properties imageOutputTokens, imageOutputCost,
and totalCost when adding these assertions.

steebchen and others added 2 commits May 7, 2026 21:03
OpenAI's official pricing table lists no text-output price for
gpt-image-2 — the model only emits image output. The stale 15/1M was a
holdover; set to 0 to match the published rate card.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a new `cachedImageInputPrice` field on `ProviderModelMapping`. For
providers whose `prompt_tokens` already includes image tokens
(OpenAI/Azure/xAI on image-output models), apportion the upstream
`cached_tokens` count between text and image by the overall image:text
ratio in `prompt_tokens`, then bill each portion at its own rate. Fixes
double-billing of image-cached tokens (previously charged at both
`imageInputPrice` and `cachedInputPrice`).

For gpt-image-2 (openai+azure):
  cachedInputPrice      $1.25/M (text-cached, was $2)
  cachedImageInputPrice $2/M    (image-cached, new field)

Also updates the "free flag" sanity test to recognise image-output
models that set `outputPrice=0` but bill via `imageOutputPrice`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/gateway/src/lib/costs.spec.ts`:
- Around line 546-549: The test currently computes discountMultiplier from a
possibly undefined azureProvider, which yields a false-positive when the lookup
fails; update the test around the models lookup so you assert that the Azure
mapping was found (e.g., assert/expect that azureProvider is defined) before
computing discountMultiplier, and fail the test if it's missing so the test
genuinely verifies Azure-discount behavior (reference the azureProvider, models
lookup, and discountMultiplier identifiers when making the change).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: afe423ad-20a0-44e7-9d80-7bf69c03e3f0

📥 Commits

Reviewing files that changed from the base of the PR and between c40ab32 and 75d6d88.

📒 Files selected for processing (5)
  • apps/gateway/src/lib/costs.spec.ts
  • apps/gateway/src/lib/costs.ts
  • packages/actions/src/models.spec.ts
  • packages/models/src/models.ts
  • packages/models/src/models/openai.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/actions/src/models.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/models/src/models/openai.ts
  • apps/gateway/src/lib/costs.ts

Comment on lines +546 to +549
const azureProvider = models
.find((m) => m.id === "gpt-image-2")
?.providers.find((p) => p.providerId === "azure");
const discountMultiplier = 1 - (azureProvider?.discount ?? 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against false positives by asserting Azure model mapping exists.
If the lookup fails, discountMultiplier becomes 1 and this test no longer proves Azure-discount behavior.

💡 Proposed test hardening
 		const azureProvider = models
 			.find((m) => m.id === "gpt-image-2")
 			?.providers.find((p) => p.providerId === "azure");
+		expect(azureProvider).toBeDefined();
 		const discountMultiplier = 1 - (azureProvider?.discount ?? 0);
📝 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
const azureProvider = models
.find((m) => m.id === "gpt-image-2")
?.providers.find((p) => p.providerId === "azure");
const discountMultiplier = 1 - (azureProvider?.discount ?? 0);
const azureProvider = models
.find((m) => m.id === "gpt-image-2")
?.providers.find((p) => p.providerId === "azure");
expect(azureProvider).toBeDefined();
const discountMultiplier = 1 - (azureProvider?.discount ?? 0);
🤖 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 `@apps/gateway/src/lib/costs.spec.ts` around lines 546 - 549, The test
currently computes discountMultiplier from a possibly undefined azureProvider,
which yields a false-positive when the lookup fails; update the test around the
models lookup so you assert that the Azure mapping was found (e.g.,
assert/expect that azureProvider is defined) before computing
discountMultiplier, and fail the test if it's missing so the test genuinely
verifies Azure-discount behavior (reference the azureProvider, models lookup,
and discountMultiplier identifiers when making the change).

steebchen and others added 2 commits May 7, 2026 21:57
Type fix after merging main: the new aws-bedrock cache write test was
calling calculateCosts without nulls for the reportedImageInputTokens /
reportedImageOutputTokens slots that were inserted before the options
object on this branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@steebchen
steebchen merged commit fde7e73 into main May 7, 2026
16 of 17 checks passed
@steebchen
steebchen deleted the bill-gpt-image-2-input-tokens branch May 7, 2026 15:40
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.

3 participants