feat(ai): dashscope-rerank recipe (DashScope serves PLURAL /reranks under compatible-api) (#2644)

DashScope's OpenAI-compatible rerank endpoint lives at
{base}/compatible-api/v1/reranks — PLURAL leaf, different base path from
the embedding surface (compatible-mode). Reusing llama-server-reranker
against DashScope forces users to hand-patch the recipe's '/rerank' leaf
in node_modules, which every upgrade silently reverts (and llama.cpp
genuinely serves singular /rerank, so changing that recipe would break
real llama.cpp users).

New dedicated recipe rides the v0.40.6.1 recipe-pluggable reranker path:
- id dashscope-rerank, base_url_default compatible-api/v1 (intl), ZE wire
- path '/reranks', default_timeout_ms 30s, 5MB payload ceiling
- models: only qwen3-rerank (live-verified 200; gte-rerank-v2 is rejected
  by the compat surface with 'Unsupported model for OpenAI compatibility
  mode', so it is deliberately not listed)
- separate recipe (not a reranker touchpoint on dashscope) because
  provider_base_urls is keyed by recipe id and the two capabilities need
  different prefixes — same topology as llama-server vs
  llama-server-reranker

Tests: recipe shape smoke mirroring recipe-llama-server-reranker.test.ts
(path/timeout/payload pins, /v1/v1 concat guard, auth resolve, sibling
recipe isolation). bun test test/ai/: 322 pass / 0 fail.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Yicon
2026-07-23 15:36:46 -07:00
committed by Garry Tan
co-authored by Claude Opus 4.8
parent ca47c054b8
commit eb55c4335a
3 changed files with 142 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import type { Recipe } from '../types.ts';
/**
* Alibaba DashScope (灵积) reranker. DashScope's OpenAI-compatible surface
* splits by capability: embeddings live under `/compatible-mode/v1` (see the
* sibling `dashscope` recipe) while rerank lives under `/compatible-api/v1`
* with a PLURAL leaf — `POST {base}/reranks`. Wire shape matches ZeroEntropy:
* request `{model, query, documents, top_n?}`, response
* `{results: [{index, relevance_score}]}` — so it rides gateway.rerank()'s
* native path with only the recipe-pluggable `path` override (v0.40.6.1).
*
* This is a SEPARATE recipe rather than a reranker touchpoint on `dashscope`
* because the two capabilities need different base URLs (`compatible-mode`
* vs `compatible-api`) and `provider_base_urls` is keyed by recipe id — one
* recipe can't point embeddings and rerank at different prefixes. Same
* topology precedent as llama-server vs llama-server-reranker.
*
* Live-verified against the China endpoint (2026-07): `/reranks` with
* `qwen3-rerank` → 200 `results[].relevance_score`; `/rerank` (singular)
* → 404; `gte-rerank-v2` → 404 "Unsupported model for OpenAI compatibility
* mode" (native-API only, so it is deliberately NOT listed here).
*
* Note: the international endpoint requires a region-aware DASHSCOPE_API_KEY.
* China-region users point at https://dashscope.aliyuncs.com/compatible-api/v1
* via `provider_base_urls['dashscope-rerank']`, mirroring the embedding
* recipe's convention.
*/
export const dashscopeRerank: Recipe = {
id: 'dashscope-rerank',
name: 'Alibaba DashScope (灵积, reranker)',
tier: 'openai-compat',
implementation: 'openai-compatible',
base_url_default: 'https://dashscope-intl.aliyuncs.com/compatible-api/v1',
auth_env: {
required: ['DASHSCOPE_API_KEY'],
setup_url: 'https://help.aliyun.com/zh/model-studio/getting-started/',
},
touchpoints: {
reranker: {
// Only the model verified live on the OpenAI-compat /reranks surface.
// gte-rerank-v2 exists on DashScope's native API but the compat path
// rejects it ("Unsupported model for OpenAI compatibility mode").
models: ['qwen3-rerank'],
default_model: 'qwen3-rerank',
// Mirror ZE's defensive per-request ceiling; gateway.rerank()
// pre-flights body size and fails open.
max_payload_bytes: 5_000_000,
// PLURAL leaf under compatible-api — the whole reason this recipe
// exists. `${base_url}${path}` → `…/compatible-api/v1/reranks`.
path: '/reranks',
// Hosted API: no local warmup, but cross-region latency can exceed
// the 5s gateway default (same rationale as llama-server-reranker).
default_timeout_ms: 30_000,
},
},
setup_hint:
'Get an API key at https://help.aliyun.com/zh/model-studio/getting-started/, then ' +
'`export DASHSCOPE_API_KEY=...` and `gbrain config set search.reranker.model ' +
'dashscope-rerank:qwen3-rerank`. China-region accounts: `gbrain config set ' +
'provider_base_urls.dashscope-rerank https://dashscope.aliyuncs.com/compatible-api/v1`.',
};
+2
View File
@@ -19,6 +19,7 @@ import { together } from './together.ts';
import { llamaServer } from './llama-server.ts';
import { minimax } from './minimax.ts';
import { dashscope } from './dashscope.ts';
import { dashscopeRerank } from './dashscope-rerank.ts';
import { zhipu } from './zhipu.ts';
import { azureOpenAI } from './azure-openai.ts';
import { zeroentropyai } from './zeroentropyai.ts';
@@ -42,6 +43,7 @@ const ALL: Recipe[] = [
llamaServerReranker,
minimax,
dashscope,
dashscopeRerank,
zhipu,
azureOpenAI,
zeroentropyai,
+79
View File
@@ -0,0 +1,79 @@
/**
* dashscope-rerank recipe smoke.
*
* Sibling of recipe-llama-server-reranker.test.ts. Pins the recipe shape so:
* - id + tier + implementation + base_url stay byte-stable
* - reranker touchpoint declares the PLURAL `/reranks` leaf (the whole
* reason this recipe exists — DashScope's compatible-api surface 404s
* on singular `/rerank`) + `default_timeout_ms`
* - only live-verified models are listed (gte-rerank-v2 is native-API only
* and rejected by the OpenAI-compat surface)
*/
import { describe, expect, test } from 'bun:test';
import { getRecipe } from '../../src/core/ai/recipes/index.ts';
import { defaultResolveAuth } from '../../src/core/ai/gateway.ts';
import { AIConfigError } from '../../src/core/ai/errors.ts';
describe('recipe: dashscope-rerank', () => {
test('registered with expected shape', () => {
const r = getRecipe('dashscope-rerank');
expect(r).toBeDefined();
expect(r!.id).toBe('dashscope-rerank');
expect(r!.tier).toBe('openai-compat');
expect(r!.implementation).toBe('openai-compatible');
expect(r!.base_url_default).toBe(
'https://dashscope-intl.aliyuncs.com/compatible-api/v1',
);
expect(r!.auth_env?.required).toEqual(['DASHSCOPE_API_KEY']);
});
test('declares reranker touchpoint with PLURAL /reranks path + timeout', () => {
const r = getRecipe('dashscope-rerank')!;
const tp = r.touchpoints.reranker;
expect(tp).toBeDefined();
expect(tp!.path).toBe('/reranks');
expect(tp!.default_timeout_ms).toBe(30_000);
expect(tp!.max_payload_bytes).toBe(5_000_000);
});
test('base_url + path concatenation produces /v1/reranks, NOT /v1/v1/…', () => {
const r = getRecipe('dashscope-rerank')!;
const combined =
r.base_url_default!.replace(/\/$/, '') + (r.touchpoints.reranker!.path ?? '/models/rerank');
expect(combined).toBe('https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks');
expect(combined).not.toContain('/v1/v1/');
expect(combined.endsWith('/reranks')).toBe(true);
});
test('lists only the live-verified compat-surface model', () => {
const r = getRecipe('dashscope-rerank')!;
const tp = r.touchpoints.reranker!;
expect(tp.models).toEqual(['qwen3-rerank']);
expect(tp.default_model).toBe('qwen3-rerank');
// gte-rerank-v2 is native-API only; the compat surface rejects it.
expect(tp.models).not.toContain('gte-rerank-v2');
});
test('default auth: DASHSCOPE_API_KEY set → Bearer token', () => {
const r = getRecipe('dashscope-rerank')!;
const auth = defaultResolveAuth(
r,
{ DASHSCOPE_API_KEY: 'sk-dashscope-fake' },
'reranker',
);
expect(auth.headerName).toBe('Authorization');
expect(auth.token).toBe('Bearer sk-dashscope-fake');
});
test('default auth: missing DASHSCOPE_API_KEY → AIConfigError', () => {
const r = getRecipe('dashscope-rerank')!;
expect(() => defaultResolveAuth(r, {}, 'reranker')).toThrow(AIConfigError);
});
test('does not perturb the sibling dashscope embedding recipe', () => {
const emb = getRecipe('dashscope')!;
expect(emb.base_url_default).toBe('https://dashscope-intl.aliyuncs.com/compatible-mode/v1');
expect(emb.touchpoints.reranker).toBeUndefined();
});
});