From ce05aef3deadcdac0e6fa68fad3770bda8a11f72 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 9 May 2026 23:43:02 -0700 Subject: [PATCH] feat(ai): add Alibaba DashScope recipe (#59 split, part 1/2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/core/ai/recipes/dashscope.ts | 42 +++++++++++++++++++++++ src/core/ai/recipes/index.ts | 2 ++ test/ai/recipe-dashscope.test.ts | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/core/ai/recipes/dashscope.ts create mode 100644 test/ai/recipe-dashscope.test.ts diff --git a/src/core/ai/recipes/dashscope.ts b/src/core/ai/recipes/dashscope.ts new file mode 100644 index 000000000..7aa2c273f --- /dev/null +++ b/src/core/ai/recipes/dashscope.ts @@ -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=...`', +}; diff --git a/src/core/ai/recipes/index.ts b/src/core/ai/recipes/index.ts index 5bf80db40..3ebe56361 100644 --- a/src/core/ai/recipes/index.ts +++ b/src/core/ai/recipes/index.ts @@ -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. */ diff --git a/test/ai/recipe-dashscope.test.ts b/test/ai/recipe-dashscope.test.ts new file mode 100644 index 000000000..eb6dc7562 --- /dev/null +++ b/test/ai/recipe-dashscope.test.ts @@ -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 "', () => { + 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); + }); +});