Skip to content

fix: count tokens request building - #5620

Merged
akshaydeo merged 2 commits into
devfrom
07-28-fix_count_tokens_request_building
Jul 29, 2026
Merged

fix: count tokens request building#5620
akshaydeo merged 2 commits into
devfrom
07-28-fix_count_tokens_request_building

Conversation

@TejasGhatte

@TejasGhatte TejasGhatte commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

The Gemini countTokens endpoint requires all prompt configuration (system instruction, tools, tool config, generation config) to be nested inside a generateContentRequest envelope rather than at the top level. Previously, these fields were simply stripped before sending, meaning token counts were incomplete. This PR rewrites the request body into the correct envelope shape so the full prompt is counted accurately.

Changes

  • Introduced wrapGeminiCountTokensBody which rewrites any flat generateContent-style body into the generateContentRequest envelope, handles already-enveloped bodies without double-wrapping, qualifies the model name with the models/ prefix, and strips fields not accepted by GenerateContentRequest (e.g. labels, fallbacks).
  • Added GeminiCountTokensRequest type that accepts both bare contents and the generateContentRequest envelope, with a ToGeminiGenerationRequest converter that resolves precedence between the two.
  • Replaced the previous approach of deleting toolConfig, generationConfig, and systemInstruction from the top-level body with the envelope wrapping strategy, which preserves those fields inside the envelope instead.
  • Extracted AddModalityTokens as a shared helper that maps Gemini modality strings (audio, image, video, text) to the neutral ResponsesResponseInputTokens fields, replacing duplicated inline logic in both ToBifrostCountTokensResponse and the Vertex equivalent.
  • Extended ToGeminiCountTokensResponse to round-trip PromptTokensDetails per modality and to respect TotalTokens when present.
  • Updated VertexCountTokensResponse to include PromptTokensDetails and TotalBillableCharacters, and wired AddModalityTokens into the Vertex count tokens response converter.
  • Added SetRawJSONField to provider utils to insert pre-encoded JSON verbatim without re-marshaling.
  • Updated the GenAI HTTP transport to parse count tokens requests into GeminiCountTokensRequest and route them through the new converter, including raw body passthrough for explicit Gemini requests.
  • Added comprehensive tests for wrapGeminiCountTokensBody and ToGeminiGenerationRequest covering envelope wrapping, double-wrap prevention, model prefix normalization, unsupported field stripping, and content fallback behavior.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (React)
  • Docs

How to test

go test ./core/providers/gemini/... ./core/providers/vertex/... ./transports/bifrost-http/...

Send a countTokens request with a system instruction and tools attached and verify the returned token count reflects the full prompt rather than only the contents array. Confirm that a body already wrapped in generateContentRequest is not double-wrapped by inspecting the outgoing request payload.

Screenshots/Recordings

N/A

Breaking changes

  • Yes
  • No

Related issues

N/A

Security considerations

No auth, secrets, or PII implications. The change only affects how request bodies are serialized before being forwarded to the Gemini and Vertex APIs.

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Gemini count-token requests now use a dedicated request type, normalized envelopes, and early model validation. Gemini and Vertex response conversion now aggregates modality token details while separating cached read tokens.

Changes

Gemini Count Tokens

Layer / File(s) Summary
Count-token request contract and routing
core/providers/gemini/types.go, core/providers/utils/utils.go, transports/bifrost-http/integrations/genai.go
Count-token requests receive a dedicated type, route-derived model handling, raw-body preservation, and raw JSON field updates.
Request normalization and generation conversion
core/providers/gemini/gemini.go, core/providers/gemini/count_tokens.go, core/providers/gemini/payload_ordering_test.go
Gemini validates models early, wraps bodies under generateContentRequest, removes unsupported fields, normalizes model names, converts request shapes, and tests precedence and immutability behavior.
Modality token response mapping
core/providers/gemini/count_tokens.go, core/providers/vertex/types.go, core/providers/vertex/count_tokens.go, core/providers/gemini/payload_ordering_test.go
Gemini and Vertex responses aggregate prompt modality counts, keep cached read tokens separate, and emit positive modality details with cached-token coverage tests.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GenAIRoute
  participant GeminiCountTokens
  participant GeminiAPI
  GenAIRoute->>GeminiCountTokens: create and populate count-token request
  GeminiCountTokens->>GeminiCountTokens: validate model and normalize body
  GeminiCountTokens->>GeminiAPI: send countTokens request
Loading

Suggested reviewers: akshaydeo, pratham-mishra04, roroghost17

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 90.91% 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.
Title check ✅ Passed The title is concise and accurately reflects the main change: Gemini count-tokens request building.
Description check ✅ Passed The PR description follows the template and covers summary, changes, testing, impacted areas, and key checklist sections.
✨ 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 07-28-fix_count_tokens_request_building

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

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


tejas ghatte seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

TejasGhatte commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@TejasGhatte
TejasGhatte marked this pull request as ready for review July 28, 2026 12:59

@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: 2

🤖 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 `@core/providers/gemini/payload_ordering_test.go`:
- Around line 134-175: Extend TestGeminiCountTokensRequestToGenerationRequest
with a case that sets top-level GeminiCountTokensRequest.Fallbacks, calls
ToGeminiGenerationRequest, and asserts the resulting GeminiGenerationRequest
retains the same fallback chain.

In `@core/providers/gemini/types.go`:
- Around line 2422-2426: Add a top-level Fallbacks field to
GeminiCountTokensRequest in core/providers/gemini/types.go at lines 2422-2426,
copy non-empty request.Fallbacks into generationRequest.Fallbacks in
core/providers/gemini/count_tokens.go at lines 64-69, and update
core/providers/gemini/payload_ordering_test.go at lines 134-175 to verify
top-level fallbacks survive flattening.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a575976-bf46-4cd0-b494-c84e14161668

📥 Commits

Reviewing files that changed from the base of the PR and between 9adeb83 and c9edcc6.

📒 Files selected for processing (8)
  • core/providers/gemini/count_tokens.go
  • core/providers/gemini/gemini.go
  • core/providers/gemini/payload_ordering_test.go
  • core/providers/gemini/types.go
  • core/providers/utils/utils.go
  • core/providers/vertex/count_tokens.go
  • core/providers/vertex/types.go
  • transports/bifrost-http/integrations/genai.go

Comment thread core/providers/gemini/payload_ordering_test.go Outdated
Comment thread core/providers/gemini/types.go
@TejasGhatte
TejasGhatte force-pushed the 07-28-fix_count_tokens_request_building branch from c9edcc6 to 6ee7c1f Compare July 29, 2026 07:27

@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: 2

🤖 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 `@core/providers/gemini/count_tokens.go`:
- Around line 107-108: Update the modality-detail aggregation around
AddModalityTokens so CacheTokensDetails is processed unconditionally, including
when CachedContentTokenCount is nonzero. Keep the top-level cached token count
authoritative while ensuring each cached modality contributes to the text,
image, and audio breakdown.

In `@transports/bifrost-http/integrations/genai.go`:
- Around line 1497-1502: Update the explicit Gemini branch in the
GeminiCountTokensRequest handling to capture and enable the raw request body
only when the configured send_back_raw_request and store_raw_request_response
policies permit it, honoring client.disable_content_logging and both per-request
override gates. Otherwise leave raw-body context unset so later handling cannot
retain or expose the count-token payload.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5ed75955-e750-407d-983f-71fcca01fc85

📥 Commits

Reviewing files that changed from the base of the PR and between c9edcc6 and 6ee7c1f.

📒 Files selected for processing (8)
  • core/providers/gemini/count_tokens.go
  • core/providers/gemini/gemini.go
  • core/providers/gemini/payload_ordering_test.go
  • core/providers/gemini/types.go
  • core/providers/utils/utils.go
  • core/providers/vertex/count_tokens.go
  • core/providers/vertex/types.go
  • transports/bifrost-http/integrations/genai.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • core/providers/gemini/types.go
  • core/providers/vertex/count_tokens.go
  • core/providers/utils/utils.go
  • core/providers/vertex/types.go
  • core/providers/gemini/gemini.go

Comment thread core/providers/gemini/count_tokens.go Outdated
Comment thread transports/bifrost-http/integrations/genai.go
@TejasGhatte
TejasGhatte force-pushed the 07-28-fix_count_tokens_request_building branch from 6ee7c1f to af8a7ee Compare July 29, 2026 07:38

@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: 2

🤖 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 `@core/providers/gemini/count_tokens.go`:
- Around line 96-109: Update the cached-token handling in the token-count
conversion flow to always pass non-nil entries from resp.CacheTokensDetails
through AddModalityTokens, preserving their modality breakdown. Keep
CachedContentTokenCount authoritative for inputDetails.CachedReadTokens, using
the existing aggregate fallback only when that count is zero.
- Around line 64-73: Update the conversion logic around generationRequest so it
copies request.GenerateContentRequest before assigning Model, IsCountTokens, or
Fallbacks. Preserve the existing default construction when the envelope is nil,
while ensuring the original request envelope remains unchanged.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dc3992ac-89fb-428c-8626-ae41aabbafd1

📥 Commits

Reviewing files that changed from the base of the PR and between 6ee7c1f and af8a7ee.

📒 Files selected for processing (8)
  • core/providers/gemini/count_tokens.go
  • core/providers/gemini/gemini.go
  • core/providers/gemini/payload_ordering_test.go
  • core/providers/gemini/types.go
  • core/providers/utils/utils.go
  • core/providers/vertex/count_tokens.go
  • core/providers/vertex/types.go
  • transports/bifrost-http/integrations/genai.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • core/providers/vertex/count_tokens.go
  • core/providers/gemini/types.go
  • core/providers/vertex/types.go
  • core/providers/gemini/payload_ordering_test.go
  • core/providers/utils/utils.go
  • transports/bifrost-http/integrations/genai.go
  • core/providers/gemini/gemini.go

Comment thread core/providers/gemini/count_tokens.go Outdated
Comment thread core/providers/gemini/count_tokens.go
@TejasGhatte
TejasGhatte force-pushed the 07-28-fix_count_tokens_request_building branch from af8a7ee to c012275 Compare July 29, 2026 09:02
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026

akshaydeo commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Merge activity

  • Jul 29, 11:07 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 29, 11:08 AM UTC: @akshaydeo merged this pull request with Graphite.

@akshaydeo
akshaydeo changed the base branch from 07-28-fix_vertex_count_tokens_unsupported_fields to graphite-base/5620 July 29, 2026 11:07
@akshaydeo
akshaydeo changed the base branch from graphite-base/5620 to dev July 29, 2026 11:07
@akshaydeo
akshaydeo dismissed coderabbitai[bot]’s stale review July 29, 2026 11:07

The base branch was changed.

@akshaydeo
akshaydeo merged commit 2c7ac0d into dev Jul 29, 2026
8 of 9 checks passed
@akshaydeo
akshaydeo deleted the 07-28-fix_count_tokens_request_building branch July 29, 2026 11:08
akshaydeo pushed a commit that referenced this pull request Jul 29, 2026
## Summary

Fixes a regression where `:countTokens` for both Gemini (`generativelanguage`) and Vertex (`aiplatform`) reported only the `contents` token count because `systemInstruction`, `generationConfig`, and `toolConfig` were unconditionally stripped from the request body. In the reported case, a ~7.4k-token system prompt plus tool declarations counted as only 17 tokens on Vertex.

The root cause is that the two endpoints require opposite request shapes:
- **Gemini**: `systemInstruction`, `tools`, `toolConfig`, and `generationConfig` are rejected as `Unknown name` at the top level and must be wrapped inside a `generateContentRequest` envelope. Top-level `contents`/`model` are silently ignored when the envelope is present.
- **Vertex**: These fields are accepted flat; only `toolConfig` (and a few others) must be stripped. There is no envelope.

The `/genai` ingress must now parse both shapes and emit whichever the resolved provider requires. The Bifrost-only `fallbacks` routing field must be honoured for routing but stripped before the upstream call.

## Changes

- Added E2E test suite **34. Gemini/Vertex countTokens full-prompt accounting** covering:
  - **34.1 / 34.6** – Contents-only baseline for Gemini and Vertex respectively, recording `totalTokens` as a collection variable for relative assertions in subsequent cases.
  - **34.2 / 34.7** – Flat `systemInstruction` is counted (not stripped) for both providers.
  - **34.3** – Flat `tools`, `toolConfig`, and `generationConfig` are wrapped into the Gemini envelope rather than forwarded verbatim (which would produce a hard 400).
  - **34.8** – Vertex keeps `tools` and `generationConfig` flat but strips `toolConfig`.
  - **34.4 / 34.9** – A `generateContentRequest` envelope is passed through for Gemini and unwrapped for Vertex.
  - **34.5** – The Bifrost `fallbacks` field is stripped before the upstream call and does not alter the counted token total.
- Assertions are relative (count must exceed the contents-only baseline) rather than absolute, so they remain valid across model version updates. A dropped field collapses the count back to baseline, which is exactly the regression signature these cases catch.
- HTTP 400 is intentionally **not** in the infra guard for the tools/toolConfig/fallbacks cases — a 400 from upstream is itself the regression signature for those scenarios.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Providers/Integrations

## How to test

Import the updated `provider-harness.json` collection into Postman or Newman and run group **34** with valid `genaiKey`, `genaiModel`, `vertexModel`, and `baseUrl` collection variables set.

```sh
newman run tests/e2e/api/collections/provider-harness.json \
  --folder "34. Gemini/Vertex countTokens full-prompt accounting (PR #5620)" \
  --env-var baseUrl=<your-base-url> \
  --env-var genaiKey=<your-api-key> \
  --env-var genaiModel=gemini-2.5-pro \
  --env-var vertexModel=gemini-2.5-pro
```

Each case should return HTTP 2xx with `totalTokens` strictly greater than the baseline recorded in 34.1/34.6. Any case that returns an `Unknown name` error or a token count equal to the baseline indicates the regression has re-appeared.

## Breaking changes

- [x] No

## Related issues

Closes #5620

## Security considerations

No auth, secrets, or PII changes. Test requests use a scoped no-op tool declaration (`probe_context`) with no real side effects.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
## Summary

The Gemini `countTokens` endpoint requires all prompt configuration (system instruction, tools, tool config, generation config) to be nested inside a `generateContentRequest` envelope rather than at the top level. Previously, these fields were simply stripped before sending, meaning token counts were incomplete. This PR rewrites the request body into the correct envelope shape so the full prompt is counted accurately.

## Changes

- Introduced `wrapGeminiCountTokensBody` which rewrites any flat `generateContent`-style body into the `generateContentRequest` envelope, handles already-enveloped bodies without double-wrapping, qualifies the model name with the `models/` prefix, and strips fields not accepted by `GenerateContentRequest` (e.g. `labels`, `fallbacks`).
- Added `GeminiCountTokensRequest` type that accepts both bare `contents` and the `generateContentRequest` envelope, with a `ToGeminiGenerationRequest` converter that resolves precedence between the two.
- Replaced the previous approach of deleting `toolConfig`, `generationConfig`, and `systemInstruction` from the top-level body with the envelope wrapping strategy, which preserves those fields inside the envelope instead.
- Extracted `AddModalityTokens` as a shared helper that maps Gemini modality strings (`audio`, `image`, `video`, text) to the neutral `ResponsesResponseInputTokens` fields, replacing duplicated inline logic in both `ToBifrostCountTokensResponse` and the Vertex equivalent.
- Extended `ToGeminiCountTokensResponse` to round-trip `PromptTokensDetails` per modality and to respect `TotalTokens` when present.
- Updated `VertexCountTokensResponse` to include `PromptTokensDetails` and `TotalBillableCharacters`, and wired `AddModalityTokens` into the Vertex count tokens response converter.
- Added `SetRawJSONField` to provider utils to insert pre-encoded JSON verbatim without re-marshaling.
- Updated the GenAI HTTP transport to parse count tokens requests into `GeminiCountTokensRequest` and route them through the new converter, including raw body passthrough for explicit Gemini requests.
- Added comprehensive tests for `wrapGeminiCountTokensBody` and `ToGeminiGenerationRequest` covering envelope wrapping, double-wrap prevention, model prefix normalization, unsupported field stripping, and content fallback behavior.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/gemini/... ./core/providers/vertex/... ./transports/bifrost-http/...
```

Send a `countTokens` request with a system instruction and tools attached and verify the returned token count reflects the full prompt rather than only the `contents` array. Confirm that a body already wrapped in `generateContentRequest` is not double-wrapped by inspecting the outgoing request payload.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

No auth, secrets, or PII implications. The change only affects how request bodies are serialized before being forwarded to the Gemini and Vertex APIs.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
akhsaul pushed a commit to akhsaul/bifrost that referenced this pull request Aug 27, 2026
## Summary

Fixes a regression where `:countTokens` for both Gemini (`generativelanguage`) and Vertex (`aiplatform`) reported only the `contents` token count because `systemInstruction`, `generationConfig`, and `toolConfig` were unconditionally stripped from the request body. In the reported case, a ~7.4k-token system prompt plus tool declarations counted as only 17 tokens on Vertex.

The root cause is that the two endpoints require opposite request shapes:
- **Gemini**: `systemInstruction`, `tools`, `toolConfig`, and `generationConfig` are rejected as `Unknown name` at the top level and must be wrapped inside a `generateContentRequest` envelope. Top-level `contents`/`model` are silently ignored when the envelope is present.
- **Vertex**: These fields are accepted flat; only `toolConfig` (and a few others) must be stripped. There is no envelope.

The `/genai` ingress must now parse both shapes and emit whichever the resolved provider requires. The Bifrost-only `fallbacks` routing field must be honoured for routing but stripped before the upstream call.

## Changes

- Added E2E test suite **34. Gemini/Vertex countTokens full-prompt accounting** covering:
  - **34.1 / 34.6** – Contents-only baseline for Gemini and Vertex respectively, recording `totalTokens` as a collection variable for relative assertions in subsequent cases.
  - **34.2 / 34.7** – Flat `systemInstruction` is counted (not stripped) for both providers.
  - **34.3** – Flat `tools`, `toolConfig`, and `generationConfig` are wrapped into the Gemini envelope rather than forwarded verbatim (which would produce a hard 400).
  - **34.8** – Vertex keeps `tools` and `generationConfig` flat but strips `toolConfig`.
  - **34.4 / 34.9** – A `generateContentRequest` envelope is passed through for Gemini and unwrapped for Vertex.
  - **34.5** – The Bifrost `fallbacks` field is stripped before the upstream call and does not alter the counted token total.
- Assertions are relative (count must exceed the contents-only baseline) rather than absolute, so they remain valid across model version updates. A dropped field collapses the count back to baseline, which is exactly the regression signature these cases catch.
- HTTP 400 is intentionally **not** in the infra guard for the tools/toolConfig/fallbacks cases — a 400 from upstream is itself the regression signature for those scenarios.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Providers/Integrations

## How to test

Import the updated `provider-harness.json` collection into Postman or Newman and run group **34** with valid `genaiKey`, `genaiModel`, `vertexModel`, and `baseUrl` collection variables set.

```sh
newman run tests/e2e/api/collections/provider-harness.json \
  --folder "34. Gemini/Vertex countTokens full-prompt accounting (PR maximhq#5620)" \
  --env-var baseUrl=<your-base-url> \
  --env-var genaiKey=<your-api-key> \
  --env-var genaiModel=gemini-2.5-pro \
  --env-var vertexModel=gemini-2.5-pro
```

Each case should return HTTP 2xx with `totalTokens` strictly greater than the baseline recorded in 34.1/34.6. Any case that returns an `Unknown name` error or a token count equal to the baseline indicates the regression has re-appeared.

## Breaking changes

- [x] No

## Related issues

Closes maximhq#5620

## Security considerations

No auth, secrets, or PII changes. Test requests use a scoped no-op tool declaration (`probe_context`) with no real side effects.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
## Summary

The Gemini `countTokens` endpoint requires all prompt configuration (system instruction, tools, tool config, generation config) to be nested inside a `generateContentRequest` envelope rather than at the top level. Previously, these fields were simply stripped before sending, meaning token counts were incomplete. This PR rewrites the request body into the correct envelope shape so the full prompt is counted accurately.

## Changes

- Introduced `wrapGeminiCountTokensBody` which rewrites any flat `generateContent`-style body into the `generateContentRequest` envelope, handles already-enveloped bodies without double-wrapping, qualifies the model name with the `models/` prefix, and strips fields not accepted by `GenerateContentRequest` (e.g. `labels`, `fallbacks`).
- Added `GeminiCountTokensRequest` type that accepts both bare `contents` and the `generateContentRequest` envelope, with a `ToGeminiGenerationRequest` converter that resolves precedence between the two.
- Replaced the previous approach of deleting `toolConfig`, `generationConfig`, and `systemInstruction` from the top-level body with the envelope wrapping strategy, which preserves those fields inside the envelope instead.
- Extracted `AddModalityTokens` as a shared helper that maps Gemini modality strings (`audio`, `image`, `video`, text) to the neutral `ResponsesResponseInputTokens` fields, replacing duplicated inline logic in both `ToBifrostCountTokensResponse` and the Vertex equivalent.
- Extended `ToGeminiCountTokensResponse` to round-trip `PromptTokensDetails` per modality and to respect `TotalTokens` when present.
- Updated `VertexCountTokensResponse` to include `PromptTokensDetails` and `TotalBillableCharacters`, and wired `AddModalityTokens` into the Vertex count tokens response converter.
- Added `SetRawJSONField` to provider utils to insert pre-encoded JSON verbatim without re-marshaling.
- Updated the GenAI HTTP transport to parse count tokens requests into `GeminiCountTokensRequest` and route them through the new converter, including raw body passthrough for explicit Gemini requests.
- Added comprehensive tests for `wrapGeminiCountTokensBody` and `ToGeminiGenerationRequest` covering envelope wrapping, double-wrap prevention, model prefix normalization, unsupported field stripping, and content fallback behavior.

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] Providers/Integrations
- [ ] Plugins
- [ ] UI (React)
- [ ] Docs

## How to test

```sh
go test ./core/providers/gemini/... ./core/providers/vertex/... ./transports/bifrost-http/...
```

Send a `countTokens` request with a system instruction and tools attached and verify the returned token count reflects the full prompt rather than only the `contents` array. Confirm that a body already wrapped in `generateContentRequest` is not double-wrapped by inspecting the outgoing request payload.

## Screenshots/Recordings

N/A

## Breaking changes

- [ ] Yes
- [x] No

## Related issues

N/A

## Security considerations

No auth, secrets, or PII implications. The change only affects how request bodies are serialized before being forwarded to the Gemini and Vertex APIs.

## Checklist

- [ ] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [ ] I verified the CI pipeline passes locally if applicable
occcat pushed a commit to occcat/bifrost that referenced this pull request Sep 2, 2026
## Summary

Fixes a regression where `:countTokens` for both Gemini (`generativelanguage`) and Vertex (`aiplatform`) reported only the `contents` token count because `systemInstruction`, `generationConfig`, and `toolConfig` were unconditionally stripped from the request body. In the reported case, a ~7.4k-token system prompt plus tool declarations counted as only 17 tokens on Vertex.

The root cause is that the two endpoints require opposite request shapes:
- **Gemini**: `systemInstruction`, `tools`, `toolConfig`, and `generationConfig` are rejected as `Unknown name` at the top level and must be wrapped inside a `generateContentRequest` envelope. Top-level `contents`/`model` are silently ignored when the envelope is present.
- **Vertex**: These fields are accepted flat; only `toolConfig` (and a few others) must be stripped. There is no envelope.

The `/genai` ingress must now parse both shapes and emit whichever the resolved provider requires. The Bifrost-only `fallbacks` routing field must be honoured for routing but stripped before the upstream call.

## Changes

- Added E2E test suite **34. Gemini/Vertex countTokens full-prompt accounting** covering:
  - **34.1 / 34.6** – Contents-only baseline for Gemini and Vertex respectively, recording `totalTokens` as a collection variable for relative assertions in subsequent cases.
  - **34.2 / 34.7** – Flat `systemInstruction` is counted (not stripped) for both providers.
  - **34.3** – Flat `tools`, `toolConfig`, and `generationConfig` are wrapped into the Gemini envelope rather than forwarded verbatim (which would produce a hard 400).
  - **34.8** – Vertex keeps `tools` and `generationConfig` flat but strips `toolConfig`.
  - **34.4 / 34.9** – A `generateContentRequest` envelope is passed through for Gemini and unwrapped for Vertex.
  - **34.5** – The Bifrost `fallbacks` field is stripped before the upstream call and does not alter the counted token total.
- Assertions are relative (count must exceed the contents-only baseline) rather than absolute, so they remain valid across model version updates. A dropped field collapses the count back to baseline, which is exactly the regression signature these cases catch.
- HTTP 400 is intentionally **not** in the infra guard for the tools/toolConfig/fallbacks cases — a 400 from upstream is itself the regression signature for those scenarios.

## Type of change

- [x] Bug fix

## Affected areas

- [x] Providers/Integrations

## How to test

Import the updated `provider-harness.json` collection into Postman or Newman and run group **34** with valid `genaiKey`, `genaiModel`, `vertexModel`, and `baseUrl` collection variables set.

```sh
newman run tests/e2e/api/collections/provider-harness.json \
  --folder "34. Gemini/Vertex countTokens full-prompt accounting (PR maximhq#5620)" \
  --env-var baseUrl=<your-base-url> \
  --env-var genaiKey=<your-api-key> \
  --env-var genaiModel=gemini-2.5-pro \
  --env-var vertexModel=gemini-2.5-pro
```

Each case should return HTTP 2xx with `totalTokens` strictly greater than the baseline recorded in 34.1/34.6. Any case that returns an `Unknown name` error or a token count equal to the baseline indicates the regression has re-appeared.

## Breaking changes

- [x] No

## Related issues

Closes maximhq#5620

## Security considerations

No auth, secrets, or PII changes. Test requests use a scoped no-op tool declaration (`probe_context`) with no real side effects.

## Checklist

- [x] I read `docs/contributing/README.md` and followed the guidelines
- [x] I added/updated tests where appropriate
- [ ] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
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