Skip to content

fix(rerank): apply per-connection proxy and strip top_n for Voyage AI - #7420

Closed
kamenkadmitry wants to merge 11 commits into
cheaper-inference:release/v3.8.49from
careerfactory:fix/rerank-proxy-and-voyage-topn
Closed

kamenkadmitry wants to merge 11 commits into
cheaper-inference:release/v3.8.49from
careerfactory:fix/rerank-proxy-and-voyage-topn

Conversation

@kamenkadmitry

Copy link
Copy Markdown
Contributor

Summary

  • Rerank handler ignored per-connection proxy pinning. handleRerank used plain fetch() without runWithProxyContext, so rerank requests always egressed directly — bypassing the SOCKS5 proxy configured on the connection. This caused failures for providers that block certain IP ranges (e.g. Voyage AI returns 403 from Russian IPs, making all rerank model tests fail while embedding tests on the same provider passed).

  • Voyage AI rejects top_n with HTTP 400. The default Cohere-compatible request adapter in transformRequestForProvider passed top_n through unconditionally. Voyage AI's rerank API does not accept this parameter and returns: "Argument 'top_n' is not supported by our API". A new "voyage" format entry strips top_n while preserving all other Cohere-compatible fields.

  • modelTestRunner did not pass connectionId to the embeddings handler during test-all, so per-connection proxy resolution was not triggered for embedding model tests either (the embeddings service resolves proxy from credentials.connectionId internally, but the options.connectionId parameter enables the same path when credentials don't carry it).

Changes

File Change
open-sse/config/rerankRegistry.ts Add format: "voyage" to voyage-ai provider entry
open-sse/handlers/rerank.ts Import resolveProxyForConnection + runWithProxyContext; add connectionId param to handleRerank; wrap fetch in runWithProxyContext when connectionId is set; add "voyage" format to transformRequestForProvider that strips top_n
src/app/api/v1/rerank/route.ts Pass credentials.connectionId to handleRerank
src/lib/api/modelTestRunner.ts Pass connectionId to handleValidatedEmbeddingRequestBody
tests/unit/rerank-providers-5332.test.ts 4 new tests: voyage-ai strips top_n, preserves body without top_n, registered with format=voyage, alias resolves correctly

Root cause analysis

The rerank handler was the only handler that didn't use the proxy infrastructure — chatCore uses runWithProxyContext via chatHelpers.ts, embeddings uses it via src/lib/embeddings/service.ts, images and count_tokens use it directly in their routes. Rerank was missed when proxy support was added across handlers.

Test plan

  • node --import tsx/esm --test tests/unit/rerank-providers-5332.test.ts — 11/11 pass (4 new)
  • node --import tsx/esm --test tests/unit/rerank.test.ts — 12/12 pass
  • node --import tsx/esm --test tests/unit/rerank-openrouter-6574.test.ts — pass
  • node --import tsx/esm --test tests/unit/embedding-rerank-provider-registry.test.ts — pass
  • npm run typecheck:core — pass
  • npx eslint on all changed files — pass
  • Pre-commit hooks (prettier, eslint, docs-sync, any-budget check, tracked-artifacts) — pass
  • Manual: verify Voyage AI rerank models pass test-all with per-connection SOCKS5 proxy

Generated with Devin

diegosouzapw and others added 8 commits July 14, 2026 13:28
…uto_merge_conditions (rules-based path is EOL 2026-07-16) (cheaper-inference#7216)
…in's copy hard-fails every PR (cheaper-inference#7341)

main's copy of this test still does git I/O inside a unit test:

    const baseSrc = git(['show', 'origin/main:' + FILE]);

Runners check out a shallow single ref, so origin/main does not resolve and the
test dies with 'fatal: invalid object name origin/main'. Every PR into main
fails Unit Tests (7/8) on it — today that is cheaper-inference#7313, cheaper-inference#7315, cheaper-inference#7316, cheaper-inference#7334, cheaper-inference#7336
and cheaper-inference#7337, six PRs red on a defect none of them introduced. cheaper-inference#7313 has no other
red at all.

release/v3.8.49 already carries a fix (2e42b8e, cheaper-inference#7174: try/catch, fetch
origin/main on demand, t.skip() when unreachable), but it only reaches main at
release time — so main stays broken for the whole cycle. Cherry-picking it would
also import a new problem: PR Test Policy classifies t.skip() as a silenced
assertion, which we watched it correctly catch on cheaper-inference#7300 today.

This is the hermetic version instead (ported from cheaper-inference#7327, which does the same for
the release branch): read the file straight off disk, compare against an empty
base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point —
and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the
runner's checkout depth can break.

The cheaper-inference#6634 regression stays covered: the guard's logic lives in
SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both
ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and
the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left
byte-identical.

Co-authored-by: growab <nekron@icloud.com>
…bers (cheaper-inference#7347)

main's ratchet had been failing --require-tighten on every PR: 11 metrics
improved but the baseline was never tightened. Same class as the cheaper-inference#6634
selfref guard — an infra fix that lands only on the release branch leaves
main red for the whole cycle, and every PR into main pays for it.

Values are the merged-coverage numbers from a run on main itself (a local
run measures ~68% vs CI's ~80%; the baseline's own note warns about that
gap). Only the 11 coverage values change — gitleaks and semgrepFindings
keep main's own state.

No changelog fragment: cheaper-inference#7326 carries it on release/v3.8.49, and a second
one here would double the entry at release time.
The rerank handler used plain fetch() without runWithProxyContext, so
per-connection proxy pinning was silently ignored — rerank requests
egressed directly instead of through the configured SOCKS5 proxy.
This caused failures for providers that block certain IP ranges
(e.g. Voyage AI returns 403 from Russian IPs).

Additionally, Voyage AI's rerank API rejects the top_n parameter with
HTTP 400 ("Argument 'top_n' is not supported by our API"). The default
Cohere-compatible request adapter passed top_n through unconditionally.
A new "voyage" format strips top_n while preserving all other fields.

Also fixes modelTestRunner to pass connectionId to the embeddings
handler during test-all, matching the pattern already used by the
embeddings route for proxy resolution.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces per-connection proxy resolution for rerank requests to ensure they honor the same proxy pinning as chat and embeddings. It also adds Voyage AI as a rerank provider with a custom format that strips the top_n parameter. The reviewer points out that simply stripping top_n ignores the user's requested limit because Voyage AI uses top_k instead of top_n. Additionally, Voyage AI returns the document field as a raw string, whereas Cohere-compatible clients expect an object with a text property. The reviewer suggests mapping top_n to top_k in the request adapter and implementing a response transformer to properly format the document field.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +54 to +57
if (providerConfig.format === "voyage") {
const { top_n: _top_n, ...rest } = body;
return rest;
}

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.

high

Voyage AI's rerank API uses top_k instead of top_n to limit the number of returned results. Simply stripping top_n means Voyage AI will return all reranked documents, ignoring the user's requested limit (which wastes bandwidth and processing, and returns more results than expected).

Additionally, Voyage AI's response format returns document as a raw string (e.g., "document text"), whereas the Cohere rerank format expects an object with a text property (e.g., {"text": "document text"}). Since the default response path returns the provider's raw response directly, clients expecting a Cohere-compatible response will fail to parse the document text.

We should:

  1. Map top_n to top_k in transformRequestForProvider.
  2. Add a response transformer in transformResponseFromProvider to format the document field as {"text": string} (or omit it if return_documents is false), and add standard Cohere metadata.

Don't forget to update the unit tests in tests/unit/rerank-providers-5332.test.ts to assert that top_k is correctly mapped and present in the transformed request.

Here is how you can implement the response transformer in transformResponseFromProvider:

if (providerConfig.format === "voyage") {
  const returnDocuments = options.return_documents !== false;
  return {
    id: data.id != null ? String(data.id) : `rerank-${Date.now()}`,
    results: (data.results || []).map((r) => ({
      index: r.index,
      relevance_score: r.relevance_score || 0,
      ...(returnDocuments && r.document != null
        ? { document: { text: String(r.document) } }
        : {}),
    })),
    meta: {
      api_version: { version: "2" },
      billed_units: { search_units: 1 },
    },
  };
}
  if (providerConfig.format === "voyage") {
    const { top_n, ...rest } = body;
    return {
      ...rest,
      ...(top_n !== undefined ? { top_k: top_n } : {}),
    };
  }

Address review feedback on cheaper-inference#7350:
- Voyage AI uses top_k, not top_n — map top_n→top_k instead of stripping
- Voyage AI returns {data:[…]} with document as raw string — add response
  transformer to map to Cohere format ({results:[…]} with document as
  {text:string} and standard meta block)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@kamenkadmitry

kamenkadmitry commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed review feedback in commit bf3baa4:

  1. top_n → top_k mapping: Voyage AI uses top_k, not top_n. Now mapping top_n→top_k instead of stripping, preserving the user's requested limit.

  2. Response transformer: Voyage AI returns {data:[…]} with document as a raw string. Added a transformer that maps to Cohere format — {results:[…]} with document as {text:string}, respects return_documents=false, and adds standard Cohere meta block.

  3. Tests updated: 13/13 pass (4 new tests for voyage response adapter + updated top_k mapping test).

Verified both fixes against the live Voyage AI API.

@gemini-code-assist please re-review

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for this — rerank was indeed the one handler that never got wired into runWithProxyContext, and the Voyage AI top_n→top_k + response-shape fix is correct after the gemini-code-assist round (nice catch on preserving the document text instead of stripping the field). Ran the full test file plus the sibling rerank/embedding/cost-header suites locally — all green, no regressions, and no overlap with #7380/#7419 (different proxy-egress endpoints).

One thing worth tightening before or shortly after merge: the new try { proxyInfo = await resolveProxyForConnection(connectionId); } catch {} swallows resolution failures completely silently. src/lib/embeddings/service.ts uses the same pattern but at least log.errors on failure — could you add a similar log line here? Otherwise a connection whose assigned proxy goes down will silently fall back to direct egress with zero trace, which is exactly the failure mode this PR is trying to fix for Voyage. Not a blocker (rerank had no proxy support at all before this PR, so it's strictly an improvement either way), just flagging for visibility.

Also, a changelog.d/fixes/7420-*.md fragment would be good to add per the repo's changelog convention if you have a moment.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@diegosouzapw
diegosouzapw changed the base branch from main to release/v3.8.49 July 17, 2026 19:58
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Hi! I tried to sync this branch with release/v3.8.49 from the maintainer side so CI would go green before merge, but pushing to the fork is blocked (403: Permission to careerfactory/OmniRoute.git denied) — the fork's org settings override the "allow edits by maintainers" flag, so this has to come from your side.

The good news: I resolved it locally and the conflicts are trivial — both are unrelated cycle drift, neither touches your actual changes. On your branch:

git fetch upstream release/v3.8.49
git merge upstream/release/v3.8.49
# both conflicts resolve by taking the release side:
git checkout upstream/release/v3.8.49 -- .mergify.yml config/quality/quality-baseline.json
git commit
git push

After that I verified locally: eslint clean, typecheck:core clean, and your own test file passes in full. Nothing in your code needed changing.

One note: the Fast Quality Gates job may still show red — that's a known base-red on the release branch itself (a stryker.conf.json tap.testFiles drift), not anything from your PR. Fix is up in #7729.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Still fix-in-place — needs a rebase to clear the conflict against the current tip. No competing fix for rerank proxy/top_n has landed since the last review.

kamenkadmitry and others added 2 commits July 19, 2026 22:30
Address maintainer feedback on cheaper-inference#7420:
- Add log.error when resolveProxyForConnection fails (was silently swallowed)
- Add changelog.d/fixes/7420-rerank-proxy-voyage-topn.md fragment

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nk-proxy-and-voyage-topn

# Conflicts:
#	.mergify.yml
#	config/quality/quality-baseline.json
@kamenkadmitry

Copy link
Copy Markdown
Contributor Author

Rebased onto release/v3.8.49 — conflicts in .mergify.yml and quality-baseline.json resolved by taking the release side. Also addressed your feedback:

  1. Added log.error on resolveProxyForConnection failure (was silently swallowed).
  2. Added changelog fragment changelog.d/fixes/7420-rerank-proxy-voyage-topn.md.

Tests: 18/18 pass (rerank-providers-5332 + rerank). Pre-commit hooks all green.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks for this — and after reconciling it against the other open Voyage PRs, here's where it lands: this PR bundles two unrelated fixes, and one of them should be dropped while the other is genuinely valuable and unduplicated. 🙏

Please DROP (superseded): the format: "voyage" adapter hunks in open-sse/config/rerankRegistry.ts and open-sse/handlers/rerank.ts, plus the corresponding #7350 test block in rerank-providers-5332.test.ts. #7813 is landing that same adapter. Worth noting for accuracy: that test block asserts each data[] entry carries a document field, but the real Voyage response shape is {object, data:[{index, relevance_score}], model, usage} — no document field (independently verified by two other PRs), so this portion would have needed fixing anyway.

Please KEEP (this is the valuable part, and nothing else covers it): the per-connection proxy pinning — the connectionId parameter plus runWithProxyContext wrapping in handleRerank, src/app/api/v1/rerank/route.ts passing credentials.connectionId, and the src/lib/api/modelTestRunner.ts fix where the embeddings test path never forwarded connectionId to handleValidatedEmbeddingRequestBody. That last one is a real gap that's still present on the release tip — good catch.

So: rebase onto release/v3.8.49 after #7813 lands, drop the format layer, keep the proxy/egress layer, and this is ready. Thanks for digging into both layers — the proxy fix is the kind of thing that's easy to miss. 🚀

diegosouzapw added a commit that referenced this pull request Jul 20, 2026
…) (#7867)

Rerank egressed directly while chat and embeddings on the SAME connection went
through the connection's proxy, so a provider that geo-blocks the host IP
(Voyage AI) failed on a connection that was otherwise working. handleRerank now
takes a connectionId, resolves that connection's proxy and wraps the upstream
fetch in runWithProxyContext; a failed lookup is logged and skipped rather than
turned into a request error. Also threads connectionId into the embeddings path
of runSingleModelTest, which had the same gap.

The change is lifted from #7420 by @kamenkadmitry. That PR could not be updated
in place: its head lives on an organization-owned fork, where GitHub's 'allow
edits from maintainers' does not grant push access, and its branch had drifted
~3 weeks (343 files of formatting churn once merged with the tip). Only the
proxy layer is taken here — #7420's voyage format adapter is superseded by
#7813 and was factually wrong about the Voyage response shape.

Refs #7350
Refs #7420

Co-authored-by: kamenkadmitry <kamenkadmitry@users.noreply.github.com>
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thank you for this one, @kamenkadmitry — the proxy-pinning half was a real, unmerged gap and it is now on release/v3.8.49.

Why this PR is being closed rather than merged, in full detail so nothing here reads as a dismissal:

  1. We could not update your branch. Its head lives on careerfactory/OmniRoute, an organization-owned fork. GitHub's allow edits from maintainers only grants push access on forks owned by a user account — maintainerCanModify reports true, but repos/careerfactory/OmniRoute returns push: false, and every push attempt 403s. So the usual fix-in-place path was unavailable to us.

  2. The branch had drifted ~3 weeks. Merging it with the current tip produced 343 files / +5033 −4325, almost entirely Prettier reformatting churn — impossible to review as a unit and impossible to land safely.

  3. Half of it was superseded. The format: "voyage" adapter in rerankRegistry.ts/rerank.ts overlapped fix(rerank): add voyage format adapter for request/response translation (#7809) #7813, and its test asserted a document field per data[] entry that the real Voyage API does not return — so that half would have shipped a wrong response shape.

What shipped, from your work: #7867 carries your proxy layer verbatim — handleRerank taking a connectionId, resolving the connection's proxy and wrapping the upstream fetch in runWithProxyContext; /v1/rerank passing credentials.connectionId; and the runSingleModelTest embeddings gap-fix. You are the Co-authored-by on that commit. We added the regression test the layer was missing (tests/unit/rerank-proxy-pinning-7350.test.ts, proven red-then-green).

If you hit this again, opening the PR from a personal fork rather than the org one lets us fix things in place and keeps your branch as the thing that merges. Sorry for the extra hop — the diagnosis and the fix were both yours.

HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…aper-inference#7350) (cheaper-inference#7867)

Rerank egressed directly while chat and embeddings on the SAME connection went
through the connection's proxy, so a provider that geo-blocks the host IP
(Voyage AI) failed on a connection that was otherwise working. handleRerank now
takes a connectionId, resolves that connection's proxy and wraps the upstream
fetch in runWithProxyContext; a failed lookup is logged and skipped rather than
turned into a request error. Also threads connectionId into the embeddings path
of runSingleModelTest, which had the same gap.

The change is lifted from cheaper-inference#7420 by @kamenkadmitry. That PR could not be updated
in place: its head lives on an organization-owned fork, where GitHub's 'allow
edits from maintainers' does not grant push access, and its branch had drifted
~3 weeks (343 files of formatting churn once merged with the tip). Only the
proxy layer is taken here — cheaper-inference#7420's voyage format adapter is superseded by
cheaper-inference#7813 and was factually wrong about the Voyage response shape.

Refs cheaper-inference#7350
Refs cheaper-inference#7420

Co-authored-by: kamenkadmitry <kamenkadmitry@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…aper-inference#7350) (cheaper-inference#7867)

Rerank egressed directly while chat and embeddings on the SAME connection went
through the connection's proxy, so a provider that geo-blocks the host IP
(Voyage AI) failed on a connection that was otherwise working. handleRerank now
takes a connectionId, resolves that connection's proxy and wraps the upstream
fetch in runWithProxyContext; a failed lookup is logged and skipped rather than
turned into a request error. Also threads connectionId into the embeddings path
of runSingleModelTest, which had the same gap.

The change is lifted from cheaper-inference#7420 by @kamenkadmitry. That PR could not be updated
in place: its head lives on an organization-owned fork, where GitHub's 'allow
edits from maintainers' does not grant push access, and its branch had drifted
~3 weeks (343 files of formatting churn once merged with the tip). Only the
proxy layer is taken here — cheaper-inference#7420's voyage format adapter is superseded by
cheaper-inference#7813 and was factually wrong about the Voyage response shape.

Refs cheaper-inference#7350
Refs cheaper-inference#7420

Co-authored-by: kamenkadmitry <kamenkadmitry@users.noreply.github.com>
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.

2 participants