Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan.
#
# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE
# identity. The manual merge-train validated batches by hand; this queue automates
# it with batching + automatic batch bisection (a red batch of N costs ~log2(N)
# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo.
#
# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's
# pre-merge ⭐ gate):
# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a
# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label
# IS the merge approval; Mergify only executes it.
# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs
# targeting the frozen branch — the freeze is a human-honored coordination signal
# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21).
# • Never label a PR another session is actively working (Hard Rule #22b).
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.

# Auto-enqueue (current Mergify model, 2026): auto_merge_conditions in
# merge_protections_settings — the rules-based queue action / autoqueue path is
# deprecated (EOL 2026-07-16). The owner-applied `queue` label IS the approval.
merge_protections_settings:
auto_merge_conditions:
- label = queue

queue_rules:
- name: release
# Any current or future release branch — the reason GitHub's native queue was
# rejected (no wildcard support on personal-account repos).
queue_conditions:
- base~=^release/v\d+\.\d+\.\d+$
- label=queue
- -draft
- -conflict
# "Everything that ran is green, nothing still running, AND the always-on
# anchor check succeeded" — robust to the path-filtered fast-gates (docs-only
# PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with
# zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY
# non-draft PR (quality.yml) and must be an affirmative success. Review approval
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
# approval in this repo's single-maintainer model (see governance header).
merge_conditions:
# "Zero failures" — EXCEPT the advisory dast-smoke: it is continue-on-error by
# design and its GH-hosted CLI build hangs recurrently (drafts #7184/#7221 were
# dequeued solely by it). Any OTHER failure still blocks (anti-fail-open kept).
- or:
- "#check-failure=0"
- and:
- "#check-failure=1"
- check-failure=dast-smoke
- "#check-pending=0"
- "#check-success>=1"
- check-success=Merge integrity (changelog + generated skills)
# NO batching: 'Merge Queue Batch' requires a paid Mergify tier (live finding
# 2026-07-15 — the queue command fails with "Cannot use Merge Queue batch" on
# the free plan). Serial queue (1 PR at a time) still automates the train.
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
merge_method: squash

pull_request_rules:
- name: clean up the queue label after merge
conditions:
- merged
actions:
label:
remove:
- queue
22 changes: 11 additions & 11 deletions config/quality/quality-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,22 @@
"eps": 0
},
"coverage.statements": {
"value": 76.5,
"value": 80.8,
"direction": "up",
"tightenSlack": 5
},
"coverage.lines": {
"value": 76.5,
"value": 80.8,
"direction": "up",
"tightenSlack": 5
},
"coverage.functions": {
"value": 82,
"value": 86.44,
"direction": "up",
"tightenSlack": 5
},
"coverage.branches": {
"value": 73,
"value": 78.1,
"direction": "up",
"eps": 1.5,
"tightenSlack": 5
Expand All @@ -54,43 +54,43 @@
"tightenSlack": 10
},
"coverage.combo.lines": {
"value": 80,
"value": 85.42,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.accountFallback.lines": {
"value": 88,
"value": 96.78,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.auth.lines": {
"value": 90,
"value": 92.55,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.routeGuard.lines": {
"value": 94,
"value": 98.73,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.error.lines": {
"value": 88,
"value": 92.13,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.publicCreds.lines": {
"value": 92,
"value": 99.07,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
},
"coverage.circuitBreaker.lines": {
"value": 92,
"value": 95.09,
"direction": "up",
"eps": 1.5,
"tightenSlack": 10
Expand Down
1 change: 1 addition & 0 deletions open-sse/config/rerankRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const RERANK_PROVIDERS = {
baseUrl: "https://api.voyageai.com/v1/rerank",
authType: "apikey",
authHeader: "bearer",
format: "voyage", // Voyage AI is Cohere-compatible except it rejects top_n (#7350)
models: [
{ id: "rerank-2.5", name: "Rerank 2.5" },
{ id: "rerank-2.5-lite", name: "Rerank 2.5 Lite" },
Expand Down
29 changes: 27 additions & 2 deletions open-sse/handlers/rerank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { attachOmniRouteMetaHeaders } from "@/domain/omnirouteResponseMeta";
import { calculateModalCost } from "@/lib/usage/costCalculator";
import { generateRequestId } from "@/shared/utils/requestId";
import { saveCallLog } from "@/lib/usageDb";
import { resolveProxyForConnection } from "@/lib/db/settings";
import { runWithProxyContext } from "../utils/proxyFetch.ts";

/**
* Build authorization header for a rerank provider
Expand Down Expand Up @@ -47,6 +49,12 @@ function buildAuthHeader(providerConfig, token) {
),
};
}
// Voyage AI is Cohere-compatible except it rejects top_n with HTTP 400 (#7350).
// Strip top_n and pass through everything else.
if (providerConfig.format === "voyage") {
const { top_n: _top_n, ...rest } = body;
return rest;
}
Comment on lines +55 to +61

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 } : {}),
    };
  }

// Default: Cohere-compatible format (used by Together, Fireworks, Cohere, SiliconFlow)
return body;
}
Expand Down Expand Up @@ -107,6 +115,7 @@ function buildAuthHeader(providerConfig, token) {
* @param {number} [options.top_n] - Number of top results to return
* @param {boolean} [options.return_documents] - Whether to include document text in results
* @param {Object} options.credentials - Provider credentials { apiKey, accessToken }
* @param {string} [options.connectionId] - Connection ID for per-connection proxy resolution
* @returns {Response}
*/
/** @returns {Promise<unknown>} */
Expand All @@ -117,6 +126,7 @@ export async function handleRerank({
top_n,
return_documents,
credentials,
connectionId = null,
}) {
const startTime = Date.now();
if (!model) return errorResponse(400, "model is required");
Expand Down Expand Up @@ -156,8 +166,18 @@ export async function handleRerank({
? `${providerConfig.baseUrl}/${modelId}`
: providerConfig.baseUrl;

try {
const res = await fetch(rerankUrl, {
// Resolve per-connection proxy so rerank honors the same proxy pinning as chat
// and embeddings (#7350). Without this, rerank requests egress directly and fail
// when the provider blocks certain IP ranges (e.g. Voyage AI from Russian IPs).
let proxyInfo: Awaited<ReturnType<typeof resolveProxyForConnection>> | null = null;
if (connectionId) {
try {
proxyInfo = await resolveProxyForConnection(connectionId);
} catch {}
}

const doFetch = () =>
fetch(rerankUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -166,6 +186,11 @@ export async function handleRerank({
body: JSON.stringify(requestBody),
});

try {
const res = connectionId
? await runWithProxyContext(proxyInfo?.proxy || null, doFetch)
: await doFetch();

if (!res.ok) {
const errData = await res.json().catch(() => ({}));
return errorResponse(
Expand Down
1 change: 1 addition & 0 deletions src/app/api/v1/rerank/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ async function postHandler(request, context) {
top_n: body.top_n,
return_documents: body.return_documents,
credentials,
connectionId: (credentials as { connectionId?: string } | null)?.connectionId || null,
});
if (response?.ok) {
await clearRecoveredProviderState(credentials);
Expand Down
3 changes: 2 additions & 1 deletion src/lib/api/modelTestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ export async function runSingleModelTest(
const runInner = async (signal: AbortSignal): Promise<Response> => {
if (isEmbedding) {
return handleValidatedEmbeddingRequestBody(
testBody as Record<string, unknown> & { model: string }
testBody as Record<string, unknown> & { model: string },
{ connectionId: connectionId || undefined }
);
}
if (isRerank) {
Expand Down
23 changes: 13 additions & 10 deletions tests/unit/check-test-masking-selfref-6634.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
* (`if (file.endsWith("check-test-masking.test.ts")) continue;` in
* scripts/check/check-test-masking.mjs) for precisely this reason — this test
* asserts evaluateMasking() now applies the same exclusion for its diff-based
* tautology counters, using the real base(origin/main)/head(HEAD) diff of
* tautology counters, against the REAL current source of
* tests/unit/check-test-masking.test.ts.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import {
countTautologies,
Expand All @@ -28,16 +30,17 @@ import {
} from "../../scripts/check/check-test-masking.mjs";

const FILE = "tests/unit/check-test-masking.test.ts";

function git(args: string[]): string {
return execFileSync("git", args, { encoding: "utf8" });
}
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");

test("#6634: check-test-masking.test.ts's own tautology fixtures must not self-flag as weakening", () => {
// origin/main predates the #6404 fixtures (countBareTautologies/scanBareTautologies
// tests) that legitimately embed tautology-pattern literals as string fixtures.
const baseSrc = git(["show", "origin/main:" + FILE]);
const headSrc = git(["show", "HEAD:" + FILE]);
// Read the REAL current source from disk rather than a git ref: the Unit Tests
// job checks out a shallow/single-ref tree with no origin/main, so `git show
// origin/main:<file>` failed the shard before it ever exercised the masking
// behavior under test. An empty base models the file's pre-#6404 state (no
// fixtures), which maximizes headTaut - baseTaut — the strictest input for the
// exclusion this test asserts.
const baseSrc = "";
const headSrc = fs.readFileSync(path.join(REPO_ROOT, FILE), "utf8");

const perFile = [
{
Expand Down
55 changes: 49 additions & 6 deletions tests/unit/rerank-providers-5332.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";

const { parseRerankModel, getAllRerankModels, getRerankProvider } = await import(
"../../open-sse/config/rerankRegistry.ts"
);
const { transformResponseFromProvider, transformRequestForProvider } = await import(
"../../open-sse/handlers/rerank.ts"
);
const { parseRerankModel, getAllRerankModels, getRerankProvider } =
await import("../../open-sse/config/rerankRegistry.ts");
const { transformResponseFromProvider, transformRequestForProvider } =
await import("../../open-sse/handlers/rerank.ts");

test("#5332 parseRerankModel resolves siliconflow multi-slash model id", () => {
assert.deepEqual(parseRerankModel("siliconflow/Qwen/Qwen3-Reranker-8B"), {
Expand Down Expand Up @@ -69,3 +67,48 @@ test("#5332 deepinfra response omits document text when return_documents=false",
assert.equal(out.results[0].document, undefined);
assert.equal(out.results[0].index, 1);
});

// #7350: Voyage AI rejects top_n with HTTP 400 — strip it in the request adapter.
test("#7350 voyage-ai request adapter strips top_n (Voyage API rejects it)", () => {
const cfg = getRerankProvider("voyage-ai");
assert.equal(cfg.format, "voyage");
const out = transformRequestForProvider(cfg, {
model: "rerank-2.5",
query: "q",
documents: ["a", "b"],
top_n: 1,
return_documents: false,
});
assert.equal("top_n" in out, false, "top_n must be stripped for voyage-ai");
assert.equal(out.model, "rerank-2.5");
assert.equal(out.query, "q");
assert.deepEqual(out.documents, ["a", "b"]);
assert.equal(out.return_documents, false);
});

test("#7350 voyage-ai request adapter preserves body when top_n is absent", () => {
const cfg = getRerankProvider("voyage-ai");
const out = transformRequestForProvider(cfg, {
model: "rerank-2.5-lite",
query: "q",
documents: ["a"],
return_documents: true,
});
assert.deepEqual(out, {
model: "rerank-2.5-lite",
query: "q",
documents: ["a"],
return_documents: true,
});
});

test("#7350 voyage-ai is registered with format=voyage", () => {
const cfg = getRerankProvider("voyage-ai");
assert.equal(cfg.format, "voyage");
assert.equal(cfg.baseUrl, "https://api.voyageai.com/v1/rerank");
});

test("#7350 voyage alias resolves to voyage-ai with format=voyage", () => {
const cfg = getRerankProvider("voyage");
assert.equal(cfg?.format, "voyage");
});