From f320e4c7fda80348cfbee14943abf7a4055da217 Mon Sep 17 00:00:00 2001 From: Noetherly <280958447+noetherly@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:08:51 +0800 Subject: [PATCH] fix(dims): handle prefixed model IDs on openai-compatible path (#2325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenRouter (and potentially other proxy providers) expose OpenAI's text-embedding-3 models with a provider prefix in the model ID, e.g. `openai/text-embedding-3-large` rather than bare `text-embedding-3-large`. `dimsProviderOptions()` checks `modelId.startsWith('text-embedding-3')` which fails for the prefixed form, so the `dimensions` parameter is never sent. The upstream provider returns its native dimensionality (3072 for -large) instead of the configured value (e.g. 1536), causing an immediate "dim mismatch" error on first embed. The default OpenRouter embedding (`text-embedding-3-small` at 1536d) masked this because its native size happens to match the default config. The bug surfaces when using `-large`, or `-small` with a non-1536 dim (512, 768, 1024 — all listed in the recipe's `dims_options`). Fix: strip the provider prefix before the `startsWith` check. The full prefixed ID is preserved in the error message for user clarity. --- src/core/ai/dims.ts | 7 ++++--- test/ai/dims-openai.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/core/ai/dims.ts b/src/core/ai/dims.ts index b88a4e175..ab8b7ab95 100644 --- a/src/core/ai/dims.ts +++ b/src/core/ai/dims.ts @@ -244,9 +244,10 @@ export function dimsProviderOptions( // configured for a smaller width (e.g. 1536) hard-fail at first embed. // Azure/OpenAI-compat embeddings are symmetric — inputType ignored. // v0.36.0.0 (D13): same range validation as native-openai path. - if (modelId.startsWith('text-embedding-3')) { - if (isOpenAITextEmbedding3Model(modelId) && !isValidOpenAITextEmbedding3Dim(modelId, dims)) { - const max = maxOpenAITextEmbedding3Dim(modelId)!; + const bareModelId = modelId.includes('/') ? modelId.split('/').pop()! : modelId; + if (bareModelId.startsWith('text-embedding-3')) { + if (isOpenAITextEmbedding3Model(bareModelId) && !isValidOpenAITextEmbedding3Dim(bareModelId, dims)) { + const max = maxOpenAITextEmbedding3Dim(bareModelId)!; throw new AIConfigError( `OpenAI model "${modelId}" supports embedding_dimensions in 1..${max}, got ${dims}.`, `Set \`embedding_dimensions\` to a value between 1 and ${max} ` + diff --git a/test/ai/dims-openai.test.ts b/test/ai/dims-openai.test.ts index c1359fdfc..2d05a4dcb 100644 --- a/test/ai/dims-openai.test.ts +++ b/test/ai/dims-openai.test.ts @@ -134,3 +134,30 @@ describe('dimsProviderOptions — OpenAI on openai-compatible adapter (Azure cas expect(JSON.stringify(opts)).not.toContain('input_type'); }); }); + +describe('dimsProviderOptions — prefixed model IDs (OpenRouter / proxy providers)', () => { + test('openai/text-embedding-3-large at 1536d returns dimensions=1536', () => { + const opts = dimsProviderOptions('openai-compatible', 'openai/text-embedding-3-large', 1536); + expect(opts).toEqual({ openaiCompatible: { dimensions: 1536 } }); + }); + + test('openai/text-embedding-3-small at 768d returns dimensions=768', () => { + const opts = dimsProviderOptions('openai-compatible', 'openai/text-embedding-3-small', 768); + expect(opts).toEqual({ openaiCompatible: { dimensions: 768 } }); + }); + + test('openai/text-embedding-3-large at 5000d throws AIConfigError', () => { + expect(() => dimsProviderOptions('openai-compatible', 'openai/text-embedding-3-large', 5000)) + .toThrow(AIConfigError); + }); + + test('error message preserves full prefixed model ID for clarity', () => { + try { + dimsProviderOptions('openai-compatible', 'openai/text-embedding-3-large', 5000); + throw new Error('should have thrown'); + } catch (err) { + expect(err).toBeInstanceOf(AIConfigError); + expect((err as Error).message).toContain('openai/text-embedding-3-large'); + } + }); +});