Issue: OpenRouter image calls are recorded as $0 (cost and token counts dropped at decode)
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
- Ambito
- backend-api-design, observability
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
- 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"}'
- Read the usage row:
GET /admin/usage/log?endpoint=/v1/images/generations&limit=1 - Compare it with the raw upstream response for the same request, via provider passthrough:
POST /p/openrouter/images/generationswith 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:
internal/providers/openai/images.go,(*CompatibleProvider).CreateImage, decodes the upstream body intocore.ImageGenerationResponse. ItsUsage *core.ImageUsage(internal/core/images.go) declares onlyinput_tokens/output_tokens/total_tokens/input_tokens_detailsand has no customUnmarshalJSON, so every other member of OpenRouter's usage object is dropped at decode:cost,cost_details,is_byok,completion_tokens_details, andprompt_tokens/completion_tokensthemselves (OpenRouter answers the images endpoint with the chat naming, not the Responses naming). Onlytotal_tokensmatches by name, which is why the row showstotal_tokens: 676alongside zero input/output tokens.internal/usage/images.go,extractFromImageResponse, buildsRawDatafrom that typed struct only (imagesplus token details). Since nocostkey ever reachesRawData,usage.CalculateUsageCostnever takes theopenRouterCreditCostbranch and falls through toCalculateGranularCost. Withoutput_tokens == 0there is nothing to price atoutput_image_per_mtok, and this model's catalog entry has noper_imagerate, 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. Aquality: low1024×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_mtokcan price.
Suggested fix
- Give
core.ImageUsagea customUnmarshalJSONmirroringcore.Usage: accept both naming sets (prompt_tokens/completion_tokensas aliases forinput_tokens/output_tokens, and vice versa, so OpenAI's owninput_tokens/output_tokenskeep working), carrycompletion_tokens_details.image_tokensinto the image token details, and collect unknown members into aRawUsage map[string]any. - In
extractFromImageResponse, mergeresp.Usage.RawUsageintoRawDatabeforeapplyUsageCosts, soopenRouterCreditCost/xaiTicksCost/edenAICostfire for image endpoints exactly as they do for chat. - Tests: an OpenRouter image fixture carrying
usage.costmust producecost_source: "openrouter_credits"; a fixture carryingprompt_tokens/completion_tokensmust populate input/output tokens (and therefore price viaoutput_image_per_mtokwhen no cost is present). - Rows already stored with
cost_source: "model_pricing"needPOST /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
- Include un Dockerfile o un file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di ENTERPILOT/GoModel
-
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
ENTERPILOT/GoModel#1381 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
ENTERPILOT/GoModel#1379 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 4/5 3-5 giorni Idoneità per principianti 55/100
ENTERPILOT/GoModel#1378 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 4/5 3-5 giorni Idoneità per principianti 44/100
ENTERPILOT/GoModel#1377 ·
I maintainer di solito rispondono entro 1 giorno
-
Feature: Embeddings mode in the dashboard PlaygroundForse già presa @shu-chen-dot-ca l’ha presa oggi. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
ENTERPILOT/GoModel#1350 ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di ENTERPILOT/GoModel
Issue simili
-
agent-butler-finding chore
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 88/100
jordansmall/spindrift#6695 ·
I maintainer di solito rispondono entro 1 giorno
-
skills-init: commit-SHA skill refs never start (git checkout -- <sha> reads the SHA as a pathspec)Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 84/100
kagent-dev/kagent#3571 ·
I maintainer di solito rispondono entro 1 giorno
-
book-layout.spec fails when the similar-books rail holds other projects' booksForse già presa @praetorianer777 l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
praetorianer777/GoTome#373 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 80/100
-
/shutdown/ready can fail to return an HTTP response when readiness takes longer than the server write timeoutForse già presa @floriankoch l’ha presa oggi. Apertatriage
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
envoyproxy/gateway#10562 ·
I maintainer di solito rispondono entro 1 giorno