From ead9643e82dc618dda2bbdc818e3abd45326f04b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 9 May 2026 23:54:18 -0700 Subject: [PATCH] feat(ai): add Zhipu AI (BigModel) recipe (#59 split, part 2/2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 13th recipe. embedding-3 (current) + embedding-2; 1024 default dims with Matryoshka options [256, 512, 1024, 2048]. OpenAI-compatible at open.bigmodel.cn. embedding-3 at 2048 dims exceeds pgvector's HNSW cap of 2000 — those brains fall back to exact vector scans via the existing chunkEmbeddingIndexSql policy at src/core/vector-index.ts. Default stays at 1024 (HNSW-fast); users who want maximum fidelity opt into 2048 via --embedding-dimensions and accept the slower retrieval. Tests pin the HNSW boundary: 1024 returns the index SQL, 2048 returns the skip-index/exact-scan SQL. Tests: bun test test/ai/ — 112/112 (6 new + 106 prior). Plan: ~/.claude/plans/ok-lets-turn-this-enumerated-sonnet.md (commit 7 of 11). Reworked from #59. Together with DashScope (commit 6), closes the China-region embedding gap users repeatedly reported (DashScope covers Alibaba, Zhipu covers BigModel; both ship with international endpoints by default). Co-Authored-By: Magicray1217 <267836857+Magicray1217@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) --- src/core/ai/recipes/index.ts | 2 + src/core/ai/recipes/zhipu.ts | 40 ++++++++++++++++++++ test/ai/recipe-zhipu.test.ts | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/core/ai/recipes/zhipu.ts create mode 100644 test/ai/recipe-zhipu.test.ts diff --git a/src/core/ai/recipes/index.ts b/src/core/ai/recipes/index.ts index 3ebe56361..d4810f2e8 100644 --- a/src/core/ai/recipes/index.ts +++ b/src/core/ai/recipes/index.ts @@ -18,6 +18,7 @@ import { together } from './together.ts'; import { llamaServer } from './llama-server.ts'; import { minimax } from './minimax.ts'; import { dashscope } from './dashscope.ts'; +import { zhipu } from './zhipu.ts'; const ALL: Recipe[] = [ openai, @@ -32,6 +33,7 @@ const ALL: Recipe[] = [ llamaServer, minimax, dashscope, + zhipu, ]; /** Map from `provider:id` key to recipe. */ diff --git a/src/core/ai/recipes/zhipu.ts b/src/core/ai/recipes/zhipu.ts new file mode 100644 index 000000000..758c5c9cb --- /dev/null +++ b/src/core/ai/recipes/zhipu.ts @@ -0,0 +1,40 @@ +import type { Recipe } from '../types.ts'; + +/** + * Zhipu AI (智谱AI) BigModel Open Platform. OpenAI-compatible /embeddings + * endpoint at open.bigmodel.cn. Hosts embedding-2 (1024d) and embedding-3 + * (Matryoshka up to 2048d). + * + * embedding-3 at 2048 dims exceeds pgvector's HNSW cap of 2000 — those + * brains fall back to exact vector scans (see + * src/core/ai/vector-index.ts:PGVECTOR_HNSW_VECTOR_MAX_DIMS). v0.32 ships + * with `default_dims: 1024` (HNSW-compatible) and exposes 2048 via + * dims_options for users who want the full embedding fidelity at the + * cost of slower retrieval. + * + * Reference: https://open.bigmodel.cn/ + */ +export const zhipu: Recipe = { + id: 'zhipu', + name: 'Zhipu AI (智谱AI BigModel)', + tier: 'openai-compat', + implementation: 'openai-compatible', + base_url_default: 'https://open.bigmodel.cn/api/paas/v4', + auth_env: { + required: ['ZHIPUAI_API_KEY'], + setup_url: 'https://open.bigmodel.cn/', + }, + touchpoints: { + embedding: { + models: ['embedding-3', 'embedding-2'], + default_dims: 1024, + // 2048 exposed but breaks HNSW (exact-scan fallback). 1024/512/256 + // stay HNSW-compatible. + dims_options: [256, 512, 1024, 2048], + max_batch_tokens: 8192, + chars_per_token: 2, + }, + }, + setup_hint: + 'Get an API key at https://open.bigmodel.cn/, then `export ZHIPUAI_API_KEY=...`', +}; diff --git a/test/ai/recipe-zhipu.test.ts b/test/ai/recipe-zhipu.test.ts new file mode 100644 index 000000000..65cce385c --- /dev/null +++ b/test/ai/recipe-zhipu.test.ts @@ -0,0 +1,71 @@ +/** + * Zhipu AI (BigModel) recipe smoke (Commit 7 of the v0.32 wave). + * + * Coverage: + * - Recipe registered with expected shape + * - default auth: ZHIPUAI_API_KEY → "Bearer "; missing → AIConfigError + * - dims_options exposes [256, 512, 1024, 2048]; default 1024 (HNSW-compatible) + * - 2048-dim path falls into exact-scan branch via chunkEmbeddingIndexSql + * from src/core/vector-index.ts + */ + +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'; +import { + PGVECTOR_HNSW_VECTOR_MAX_DIMS, + chunkEmbeddingIndexSql, +} from '../../src/core/vector-index.ts'; + +describe('recipe: zhipu', () => { + test('registered with expected shape', () => { + const r = getRecipe('zhipu'); + expect(r).toBeDefined(); + expect(r!.id).toBe('zhipu'); + expect(r!.tier).toBe('openai-compat'); + expect(r!.implementation).toBe('openai-compatible'); + expect(r!.base_url_default).toBe('https://open.bigmodel.cn/api/paas/v4'); + expect(r!.auth_env?.required).toEqual(['ZHIPUAI_API_KEY']); + }); + + test('embedding touchpoint declares embedding-3 first + 1024 dims (HNSW-compatible default)', () => { + const r = getRecipe('zhipu')!; + expect(r.touchpoints.embedding).toBeDefined(); + expect(r.touchpoints.embedding!.models[0]).toBe('embedding-3'); + expect(r.touchpoints.embedding!.models).toContain('embedding-2'); + expect(r.touchpoints.embedding!.default_dims).toBe(1024); + expect(r.touchpoints.embedding!.dims_options).toEqual([256, 512, 1024, 2048]); + // The default must stay HNSW-compatible. + expect(r.touchpoints.embedding!.default_dims).toBeLessThanOrEqual( + PGVECTOR_HNSW_VECTOR_MAX_DIMS, + ); + }); + + test('default auth: ZHIPUAI_API_KEY set → "Bearer "', () => { + const r = getRecipe('zhipu')!; + const auth = defaultResolveAuth(r, { ZHIPUAI_API_KEY: 'fake-zhipu-key' }, 'embedding'); + expect(auth.headerName).toBe('Authorization'); + expect(auth.token).toBe('Bearer fake-zhipu-key'); + }); + + test('default auth: missing ZHIPUAI_API_KEY → AIConfigError', () => { + const r = getRecipe('zhipu')!; + expect(() => defaultResolveAuth(r, {}, 'embedding')).toThrow(AIConfigError); + }); + + test('2048-dim option from dims_options falls into exact-scan branch', () => { + // 2048d exceeds the HNSW cap, so chunkEmbeddingIndexSql returns the + // exact-scan-skip-index path. Users picking 2048 trade ANN speed for + // full embedding fidelity. + const sql = chunkEmbeddingIndexSql(2048); + expect(sql.toLowerCase()).toContain('skipped'); + expect(sql.toLowerCase()).toContain('hnsw'); + }); + + test('1024-dim default returns the HNSW index SQL (fast path)', () => { + const sql = chunkEmbeddingIndexSql(1024); + expect(sql.toLowerCase()).toContain('create index'); + expect(sql.toLowerCase()).toContain('hnsw'); + }); +});