mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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:
committed by
Garry Tan
co-authored by
Claude Opus 4.8
parent
ca47c054b8
commit
eb55c4335a
@@ -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`.',
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user