feat(ai): add Alibaba DashScope recipe (#59 split, part 1/2)

12th recipe. text-embedding-v3 (current) + text-embedding-v2; 1024
default dims with Matryoshka options [64, 128, 256, 512, 768, 1024].

OpenAI-compatible at dashscope-intl.aliyuncs.com. China-region users
override via cfg.base_urls['dashscope']; v0.32 ships with the
international default.

Conservative max_batch_tokens=8192 + chars_per_token=2 declared because
Alibaba doesn't publish a hard batch limit and text-embedding-v3 mixes
English + CJK heavily (CJK density closer to Voyage than OpenAI tiktoken).

Tests: bun test test/ai/ — 106/106 (5 new + 101 prior).

Plan: ~/.claude/plans/ok-lets-turn-this-enumerated-sonnet.md (commit 6
of 11). Reworked from #59 (DashScope+Zhipu split into 2 commits per
the plan; Zhipu lands next).

Co-Authored-By: Magicray1217 <267836857+Magicray1217@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-09 23:43:02 -07:00
co-authored by Magicray1217 Claude Opus 4.7
parent 1abbe8d5a0
commit ce05aef3de
3 changed files with 101 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
import type { Recipe } from '../types.ts';
/**
* Alibaba DashScope (灵积). OpenAI-compatible /embeddings endpoint at
* dashscope-intl.aliyuncs.com. Hosts text-embedding-v2 (older) and
* text-embedding-v3 (current; Matryoshka-aware up to 1024 dims).
*
* Reference: https://help.aliyun.com/zh/model-studio/getting-started/
*
* Note: the international endpoint requires a region-aware DASHSCOPE_API_KEY.
* China-region users typically point at https://dashscope.aliyuncs.com/...
* via cfg.base_urls['dashscope']. v0.32 ships with the international
* default; users override per the recipe convention.
*/
export const dashscope: Recipe = {
id: 'dashscope',
name: 'Alibaba DashScope (灵积)',
tier: 'openai-compat',
implementation: 'openai-compatible',
base_url_default: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
auth_env: {
required: ['DASHSCOPE_API_KEY'],
setup_url: 'https://help.aliyun.com/zh/model-studio/getting-started/',
},
touchpoints: {
embedding: {
models: ['text-embedding-v3', 'text-embedding-v2'],
default_dims: 1024,
dims_options: [64, 128, 256, 512, 768, 1024],
// Alibaba doesn't publish a hard batch-token cap for the OpenAI-compat
// path. Conservative declaration so the gateway pre-splits before
// hitting whatever undocumented server-side limit exists.
max_batch_tokens: 8192,
// text-embedding-v3 mixes English + CJK heavily; the tokenizer is
// closer to Voyage density than OpenAI tiktoken for CJK-dominant
// content. Conservative chars_per_token=2 leaves headroom.
chars_per_token: 2,
},
},
setup_hint:
'Get an API key at https://help.aliyun.com/zh/model-studio/getting-started/, then `export DASHSCOPE_API_KEY=...`',
};
+2
View File
@@ -17,6 +17,7 @@ import { groq } from './groq.ts';
import { together } from './together.ts';
import { llamaServer } from './llama-server.ts';
import { minimax } from './minimax.ts';
import { dashscope } from './dashscope.ts';
const ALL: Recipe[] = [
openai,
@@ -30,6 +31,7 @@ const ALL: Recipe[] = [
together,
llamaServer,
minimax,
dashscope,
];
/** Map from `provider:id` key to recipe. */
+57
View File
@@ -0,0 +1,57 @@
/**
* DashScope (Alibaba) recipe smoke (Commit 6 of the v0.32 wave).
*/
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', () => {
test('registered with expected shape', () => {
const r = getRecipe('dashscope');
expect(r).toBeDefined();
expect(r!.id).toBe('dashscope');
expect(r!.tier).toBe('openai-compat');
expect(r!.implementation).toBe('openai-compatible');
expect(r!.base_url_default).toBe(
'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
);
expect(r!.auth_env?.required).toEqual(['DASHSCOPE_API_KEY']);
});
test('embedding touchpoint declares text-embedding-v3 first + 1024 dims', () => {
const r = getRecipe('dashscope')!;
expect(r.touchpoints.embedding).toBeDefined();
expect(r.touchpoints.embedding!.models[0]).toBe('text-embedding-v3');
expect(r.touchpoints.embedding!.models).toContain('text-embedding-v2');
expect(r.touchpoints.embedding!.default_dims).toBe(1024);
expect(r.touchpoints.embedding!.dims_options).toEqual([64, 128, 256, 512, 768, 1024]);
// Matryoshka: every dims option ≤ 2000 (HNSW-compatible).
for (const d of r.touchpoints.embedding!.dims_options ?? []) {
expect(d).toBeLessThanOrEqual(2000);
}
});
test('default auth: DASHSCOPE_API_KEY set → "Bearer <key>"', () => {
const r = getRecipe('dashscope')!;
const auth = defaultResolveAuth(
r,
{ DASHSCOPE_API_KEY: 'sk-dashscope-fake' },
'embedding',
);
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')!;
expect(() => defaultResolveAuth(r, {}, 'embedding')).toThrow(AIConfigError);
});
test('declares chars_per_token + max_batch_tokens for safer batching', () => {
const r = getRecipe('dashscope')!;
expect(r.touchpoints.embedding!.max_batch_tokens).toBeGreaterThan(0);
expect(r.touchpoints.embedding!.chars_per_token).toBeGreaterThan(0);
});
});