Issue: OpenRouter image calls are recorded as $0 (cost and token counts dropped at decode)

Aperta
#1,382 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Una pull request collegata è già stata integrata.

  • #1383 di @SantiagoDePolonia — integrata

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
25/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
go

Direzione di ricerca

Read internal/core/images.go and compare its image usage decoding with internal/core/usage_json.go; then inspect internal/providers/openai/images.go and internal/usage/images.go to trace the response into usage costing. Add coverage for the OpenRouter image usage fixtures described in the issue, then run the relevant Go tests. Done means image cost and token counts survive decoding and are reflected in usage records; linked PR #1383 is already merged.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Describe the bug

Image generation through an openrouter provider is recorded in usage tracking with total_cost: 0 and cost_source: "model_pricing", even though OpenRouter returns an exact per-request cost for the same response. The same provider leg records exact costs for chat (cost_source: "openrouter_credits").

Environment

GoModel 0.2.1 (enterpilot/gomodel:0.2.1). The four files involved are byte-identical between v0.2.1 and current main/v0.2.2, so this is not fixed yet.

To reproduce

Provider config (allowlist mode, as required for image-only models that OpenRouter's /api/v1/models does not list):

providers:
  openrouter:
    type: openrouter
    api_key: "${OPENROUTER_API_KEY}"
    models:
      - openai/gpt-image-2.6-sunburst
  1. Generate one image through the gateway:
curl -sS -X POST https://<gateway>/v1/images/generations \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-image-2.6-sunburst","prompt":"a neon smiley","n":1,"quality":"low","output_format":"png"}'
  1. Read the usage row: GET /admin/usage/log?endpoint=/v1/images/generations&limit=1
  2. Compare it with the raw upstream response for the same request, via provider passthrough: POST /p/openrouter/images/generations with the same body.

Actual vs expected

upstream (POST /p/openrouter/images/generations) GoModel usage row
cost usage.cost: 0.01998, cost_details.upstream_inference_completions_cost: 0.01992 total_cost: 0, cost_source: "model_pricing"
tokens prompt_tokens: 12, completion_tokens: 664, completion_tokens_details.image_tokens: 664 input_tokens: 0, output_tokens: 0, total_tokens: 676
raw data — {"images": 1}
caveat — provider returned no token usage; set a per_image price to cost this model

Expected: cost_source: "openrouter_credits" and total_cost: 0.01998; and the client-facing response should carry OpenRouter's usage members the way chat does. The gateway's own catalog already has output_image_per_mtok: 30 for this model, which would reconstruct ~$0.01992 — it just never sees the token count.

Root cause

Two independent losses, both from core.ImageUsage being a plain struct with no raw-usage passthrough:

  1. internal/providers/openai/images.go, (*CompatibleProvider).CreateImage, decodes the upstream body into core.ImageGenerationResponse. Its Usage *core.ImageUsage (internal/core/images.go) declares only input_tokens/output_tokens/total_tokens/input_tokens_details and has no custom UnmarshalJSON, so every other member of OpenRouter's usage object is dropped at decode: cost, cost_details, is_byok, completion_tokens_details, and prompt_tokens/completion_tokens themselves (OpenRouter answers the images endpoint with the chat naming, not the Responses naming). Only total_tokens matches by name, which is why the row shows total_tokens: 676 alongside zero input/output tokens.
  2. internal/usage/images.go, extractFromImageResponse, builds RawData from that typed struct only (images plus token details). Since no cost key ever reaches RawData, usage.CalculateUsageCost never takes the openRouterCreditCost branch and falls through to CalculateGranularCost. With output_tokens == 0 there is nothing to price at output_image_per_mtok, and this model's catalog entry has no per_image rate, so the row is $0 plus the zero-usage caveat.

Chat is unaffected because core.Usage (internal/core/usage_json.go) has a custom UnmarshalJSON that sweeps unknown members into RawUsage, which usage.ExtractFromResponse merges into RawData. The image path has no equivalent — docs/advanced/images-api.mdx promises "everything else is passed through, including the usage block", which the typed decode does not do.

Impact

  • /v1/usage, the dashboard, budgets and invoices under-report every image generation by its full price. A quality: low 1024×1024 gpt-image call is ~$0.02, so this is silent 100% under-billing, not a rounding error.
  • The row also loses the generated-image token count, i.e. the only quantity output_image_per_mtok can price.

Suggested fix

  1. Give core.ImageUsage a custom UnmarshalJSON mirroring core.Usage: accept both naming sets (prompt_tokens/completion_tokens as aliases for input_tokens/output_tokens, and vice versa, so OpenAI's own input_tokens/output_tokens keep working), carry completion_tokens_details.image_tokens into the image token details, and collect unknown members into a RawUsage map[string]any.
  2. In extractFromImageResponse, merge resp.Usage.RawUsage into RawData before applyUsageCosts, so openRouterCreditCost / xaiTicksCost / edenAICost fire for image endpoints exactly as they do for chat.
  3. Tests: an OpenRouter image fixture carrying usage.cost must produce cost_source: "openrouter_credits"; a fixture carrying prompt_tokens/completion_tokens must populate input/output tokens (and therefore price via output_image_per_mtok when no cost is present).
  4. Rows already stored with cost_source: "model_pricing" need POST /admin/usage/recalculate-pricing ({"confirmation":"recalculate"}) after the fix ships.

Workaround

Set a per_image pricing override on the model (metadata.pricing.per_image in config.yaml, or the dashboard's pricing-override editor). It is only applied while the reported output tokens are zero, so it cannot double-count once the decode is fixed — but a flat per-image rate is only correct for one quality/size.

Lingua principale
Go
Stelle
1.2k
Fork
111
Merge medio
20h 15m
PR unite (30g)
140

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di ENTERPILOT/GoModel

Tutte le issue di ENTERPILOT/GoModel

Issue simili

Altre issue su Go

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.