From f0f97a08cfda08af45ecb6bfd36a944c485caa83 Mon Sep 17 00:00:00 2001 From: sabrimete Date: Sun, 25 Oct 2026 21:04:28 +0000 Subject: [PATCH] fix(ai-chat): give uncurated Alibaba catalog models a puterId Uncurated entries admitted from the DashScope catalog feed previously surfaced with their raw feed id. They now get an alibaba:/ puterId, vendor-prefixed ids keep their org, and others infer the org from the curated family sibling. Entries whose synthesized puterId collides with a curated entry are skipped so the model is not listed twice. --- .../providers/alibaba/AlibabaProvider.test.ts | 37 +++++++++++++++++++ .../ai-chat/providers/alibaba/catalog.ts | 27 +++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts b/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts index b84897e56..0a921c940 100644 --- a/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts +++ b/src/backend/drivers/ai-chat/providers/alibaba/AlibabaProvider.test.ts @@ -809,6 +809,11 @@ describe('Alibaba official catalog', () => { expect(models.some((model) => model.id === 'qwen-image-fixture')).toBe( false, ); + const fixture = models.find( + (model) => model.id === 'qwen-catalog-fixture', + )!; + expect(fixture.puterId).toBe('alibaba:qwen/qwen-catalog-fixture'); + expect(modelLookupNames(models)).toContain('qwen-catalog-fixture'); const plus = models.find((model) => model.id === 'qwen-plus')!; const original = ALIBABA_MODELS.find( (model) => model.id === 'qwen-plus', @@ -909,6 +914,38 @@ describe('Alibaba official catalog', () => { expect(model.costs).toEqual({ tokens: 1_000_000 }); }); + it.each([ + ['qwen3.9-plus-2027-01-01', 'alibaba:qwen/qwen3.9-plus-2027-01-01'], + ['kimi/kimi-k3', 'alibaba:kimi/kimi-k3'], + [ + 'deepseek-v4.1-flash-1017', + 'alibaba:deepseek/deepseek-v4.1-flash-1017', + ], + ['glm-5.3-prime', 'alibaba:zhipu/glm-5.3-prime'], + ['decision-model-preview', 'alibaba:qwen/decision-model-preview'], + ])( + 'gives uncurated model %s a puterId in the alibaba:/ form', + (id, puterId) => { + const model = mapAlibabaCatalogModel({ + model: id, + capabilities: ['TG'], + })!; + expect(model.puterId).toBe(puterId); + }, + ); + + it('skips a vendor-prefixed spelling of a curated model', () => { + const model = mapAlibabaCatalogModel({ + model: 'zhipu/glm-5.2', + capabilities: ['TG', 'Reasoning'], + inference_metadata: { + request_modality: ['Text'], + response_modality: ['Text'], + }, + }); + expect(model).toBeNull(); + }); + it('refuses inference with unverified catalog prices', async () => { seedPages(); const { provider } = makeProvider(); diff --git a/src/backend/drivers/ai-chat/providers/alibaba/catalog.ts b/src/backend/drivers/ai-chat/providers/alibaba/catalog.ts index 063a8071c..1d551405b 100644 --- a/src/backend/drivers/ai-chat/providers/alibaba/catalog.ts +++ b/src/backend/drivers/ai-chat/providers/alibaba/catalog.ts @@ -82,6 +82,23 @@ const positiveLimit = (value: unknown): number | undefined => ? value : undefined; +const chatCapabilities = ['TG', 'Reasoning', 'VU']; + +const curatedPuterIds = new Set(ALIBABA_MODELS.map((model) => model.puterId)); + +const inferPuterId = (id: string): string => { + if (id.includes('/')) return `alibaba:${id}`; + const family = id.split('-')[0]; + const sibling = ALIBABA_MODELS.find( + (model) => model.puterId && model.id.split('-')[0] === family, + ); + // Unprefixed feed ids are Model Studio's own family unless a curated + // sibling points at a third-party org. + const org = + sibling?.puterId?.slice('alibaba:'.length).split('/')[0] ?? 'qwen'; + return `alibaba:${org}/${id}`; +}; + export const mapAlibabaCatalogModel = ( raw: AlibabaApiModel, previous?: IChatModel, @@ -96,10 +113,17 @@ export const mapAlibabaCatalogModel = ( !output && !existing && !raw.capabilities?.some((capability) => - ['TG', 'Reasoning', 'VU'].includes(capability), + chatCapabilities.includes(capability), ) ) return null; + let puterId = existing?.puterId; + if (!puterId) { + puterId = inferPuterId(id); + // A feed entry spelling a curated model differently would duplicate + // the curated entry's puterId and list the model twice. + if (curatedPuterIds.has(puterId)) return null; + } const catalogPricing: AlibabaCatalogTier[] = (raw.prices ?? []).map( (tier) => { // Abbreviated ranges do not specify whether k means 1000 or 1024 tokens. @@ -149,6 +173,7 @@ export const mapAlibabaCatalogModel = ( return { ...existing, id, + puterId, name: raw.name ?? existing?.name ?? id, description: raw.description ?? existing?.description, aliases: existing?.aliases,