diff --git a/assets/magic-context.schema.json b/assets/magic-context.schema.json index a68d74801..13b5d5c4a 100644 --- a/assets/magic-context.schema.json +++ b/assets/magic-context.schema.json @@ -1760,6 +1760,12 @@ "exclusiveMinimum": 0, "maximum": 9007199254740991 }, + "dimensions": { + "description": "Optional embedding dimensions for Matryoshka models like Qwen3-Embedding (e.g. 768, 1024, 4096). When set, sent as dimensions in the embedding request body. Omitted keeps provider default.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 8192 + }, "local_runtime": { "default": "auto", "description": "Local provider only: ONNX runtime selection. 'auto' uses native under Node and uses WASM under Bun versions before 1.4.0, where Bun's NAPI teardown race can panic on quit; native is restored automatically on Bun 1.4.0+. Set 'native' only to prefer speed while accepting that pre-1.4.0 Bun crash risk, or 'wasm' to avoid loading the native addon.", diff --git a/packages/plugin/src/config/schema/magic-context.ts b/packages/plugin/src/config/schema/magic-context.ts index ef96f0fce..c0ae3d1a1 100644 --- a/packages/plugin/src/config/schema/magic-context.ts +++ b/packages/plugin/src/config/schema/magic-context.ts @@ -619,6 +619,15 @@ const BaseEmbeddingConfigSchema = z .describe( "Optional maximum input tokens for chunk embeddings. Defaults conservatively to 512 when omitted.", ), + dimensions: z + .number() + .int() + .positive() + .max(8192) + .optional() + .describe( + "Optional embedding dimensions for Matryoshka models like Qwen3-Embedding (e.g. 768, 1024, 4096). When set, sent as dimensions in the embedding request body. Omitted keeps provider default.", + ), local_runtime: z .enum(["auto", "native", "wasm"]) .default("auto") @@ -683,6 +692,7 @@ export const EmbeddingConfigSchema = BaseEmbeddingConfigSchema.transform((data) ...(inputType ? { input_type: inputType } : {}), ...(queryInputType ? { query_input_type: queryInputType } : {}), ...(truncate ? { truncate } : {}), + ...(data.dimensions ? { dimensions: data.dimensions } : {}), ...(data.max_input_tokens ? { max_input_tokens: data.max_input_tokens } : {}), }; } @@ -716,6 +726,7 @@ export const EmbeddingConfigSchema = BaseEmbeddingConfigSchema.transform((data) ...(inputType ? { input_type: inputType } : {}), ...(queryInputType ? { query_input_type: queryInputType } : {}), ...(truncate ? { truncate } : {}), + ...(data.dimensions ? { dimensions: data.dimensions } : {}), ...(data.max_input_tokens ? { max_input_tokens: data.max_input_tokens } : {}), }; } diff --git a/packages/plugin/src/features/magic-context/memory/embedding-identity.ts b/packages/plugin/src/features/magic-context/memory/embedding-identity.ts index c05eee15d..870328f24 100644 --- a/packages/plugin/src/features/magic-context/memory/embedding-identity.ts +++ b/packages/plugin/src/features/magic-context/memory/embedding-identity.ts @@ -34,6 +34,10 @@ export function getEmbeddingProviderIdentity(config: EmbeddingConfig): string { } const truncate = config.provider === "openai-compatible" ? config.truncate?.trim() : undefined; + const dimensions = + config.provider === "openai-compatible" + ? (config as unknown as { dimensions?: number }).dimensions + : undefined; // local_dtype changes the produced vectors (a quantized ONNX model emits // different embeddings than fp32), so a non-default dtype MUST fold into the // model identity — switching dtype re-embeds rather than mixing vector @@ -66,6 +70,7 @@ export function getEmbeddingProviderIdentity(config: EmbeddingConfig): string { // lazily GCs, never a destructive wipe). inputType: config.input_type?.trim() || "", ...(truncate ? { truncate } : {}), + ...(dimensions ? { dimensions } : {}), } : { provider: "local", diff --git a/packages/plugin/src/features/magic-context/memory/embedding-openai.ts b/packages/plugin/src/features/magic-context/memory/embedding-openai.ts index b7ef7d34f..552be78f7 100644 --- a/packages/plugin/src/features/magic-context/memory/embedding-openai.ts +++ b/packages/plugin/src/features/magic-context/memory/embedding-openai.ts @@ -15,6 +15,8 @@ interface OpenAICompatibleEmbeddingProviderOptions { queryInputType?: string; /** Optional `truncate` body field (e.g. NVIDIA NIM 'NONE'/'START'/'END'). */ truncate?: string; + /** Optional `dimensions` body field for Matryoshka models like Qwen3-Embedding (e.g. 768, 1024, 4096). */ + dimensions?: number; /** Maximum safe input tokens for chunk embeddings. */ maxInputTokens?: number; } @@ -142,6 +144,7 @@ export class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider { private readonly inputType: string; private readonly queryInputType: string; private readonly truncate: string; + private readonly dimensions: number | undefined; private initialized = false; // Circuit breaker state (per provider instance — resets when config @@ -166,6 +169,10 @@ export class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider { this.inputType = options.inputType?.trim() ?? ""; this.queryInputType = options.queryInputType?.trim() ?? ""; this.truncate = options.truncate?.trim() ?? ""; + this.dimensions = + typeof options.dimensions === "number" && Number.isFinite(options.dimensions) + ? Math.max(1, Math.floor(options.dimensions)) + : undefined; this.maxInputTokens = typeof options.maxInputTokens === "number" && Number.isFinite(options.maxInputTokens) ? Math.max(1, Math.floor(options.maxInputTokens)) @@ -182,6 +189,7 @@ export class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider { // different model_id than reads/GC resolve, silently zeroing results // and reaping valid vectors. ...(this.truncate ? { truncate: this.truncate } : {}), + ...(this.dimensions ? { dimensions: this.dimensions } : {}), }); } @@ -300,6 +308,7 @@ export class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider { // unaffected. ...(inputTypeForRequest ? { input_type: inputTypeForRequest } : {}), ...(this.truncate ? { truncate: this.truncate } : {}), + ...(this.dimensions ? { dimensions: this.dimensions } : {}), }), // SSRF: refuse to FOLLOW redirects. The pre-flight SSRF check only // validates the configured endpoint; default redirect-follow would diff --git a/packages/plugin/src/features/magic-context/memory/embedding.ts b/packages/plugin/src/features/magic-context/memory/embedding.ts index 1bc56f5a5..494d7c5d9 100644 --- a/packages/plugin/src/features/magic-context/memory/embedding.ts +++ b/packages/plugin/src/features/magic-context/memory/embedding.ts @@ -80,6 +80,7 @@ function resolveEmbeddingConfig(config?: EmbeddingConfig): EmbeddingConfig { ...(inputType ? { input_type: inputType } : {}), ...(queryInputType ? { query_input_type: queryInputType } : {}), ...(truncate ? { truncate } : {}), + ...(config.dimensions ? { dimensions: config.dimensions } : {}), ...(config.max_input_tokens ? { max_input_tokens: normalizeCompartmentChunkMaxInputTokens( @@ -118,6 +119,7 @@ function createProvider(config: EmbeddingConfig): EmbeddingProvider | null { inputType: config.input_type, queryInputType: config.query_input_type, truncate: config.truncate, + dimensions: (config as unknown as { dimensions?: number }).dimensions, maxInputTokens: config.max_input_tokens, }); } diff --git a/packages/plugin/src/features/magic-context/project-embedding-registry.ts b/packages/plugin/src/features/magic-context/project-embedding-registry.ts index c4455ebbf..e33bf6bda 100644 --- a/packages/plugin/src/features/magic-context/project-embedding-registry.ts +++ b/packages/plugin/src/features/magic-context/project-embedding-registry.ts @@ -422,6 +422,7 @@ function resolveEmbeddingConfig(config?: EmbeddingConfig): EmbeddingConfig { ...(inputType ? { input_type: inputType } : {}), ...(queryInputType ? { query_input_type: queryInputType } : {}), ...(truncate ? { truncate } : {}), + ...(config.dimensions ? { dimensions: config.dimensions } : {}), ...(config.max_input_tokens ? { max_input_tokens: normalizeCompartmentChunkMaxInputTokens( @@ -502,6 +503,7 @@ function createProvider( inputType: config.input_type, queryInputType: config.query_input_type, truncate: config.truncate, + dimensions: (config as unknown as { dimensions?: number }).dimensions, maxInputTokens: config.max_input_tokens, }); } @@ -585,6 +587,7 @@ function getChunkEmbeddingModelId(config: EmbeddingConfig, providerIdentity: str "max_input_tokens" in config ? config.max_input_tokens : undefined, ), truncate: config.provider === "openai-compatible" ? (config.truncate ?? "") : "", + dimensions: config.provider === "openai-compatible" ? ((config as unknown as { dimensions?: number }).dimensions ?? "") : "", }; return `${providerIdentity}:chunk:${sha256Prefix(stableStringify(chunkIdentity))}`; } diff --git a/packages/plugin/src/plugin/embedding-bootstrap-helpers.ts b/packages/plugin/src/plugin/embedding-bootstrap-helpers.ts index e04e5fb7e..2031c3a22 100644 --- a/packages/plugin/src/plugin/embedding-bootstrap-helpers.ts +++ b/packages/plugin/src/plugin/embedding-bootstrap-helpers.ts @@ -39,10 +39,11 @@ export const EMBEDDING_AFFECTING_KEYS = new Set([ // not let a broken value re-register mid-session. "embedding.input_type", "embedding.truncate", - // max_input_tokens + query_input_type fold into the chunk-embedding identity + // max_input_tokens + query_input_type + dimensions fold into the chunk-embedding identity // (getChunkEmbeddingModelId); a failed substitution on either would otherwise // register as trusted and could drive a bogus chunk identity / GC. "embedding.max_input_tokens", + "embedding.dimensions", "embedding.query_input_type", "embedding.fallback_provider", "subc", diff --git a/packages/plugin/src/plugin/embedding-routing.ts b/packages/plugin/src/plugin/embedding-routing.ts index f09d1cad5..e53c1d8e7 100644 --- a/packages/plugin/src/plugin/embedding-routing.ts +++ b/packages/plugin/src/plugin/embedding-routing.ts @@ -53,6 +53,7 @@ function fallbackConfig( const queryInputType = typeof raw.query_input_type === "string" ? raw.query_input_type.trim() : ""; const truncate = typeof raw.truncate === "string" ? raw.truncate.trim() : ""; + const dimensions = typeof raw.dimensions === "number" ? raw.dimensions : undefined; const maxInputTokens = typeof raw.max_input_tokens === "number" ? raw.max_input_tokens : undefined; @@ -66,6 +67,7 @@ function fallbackConfig( ...(inputType ? { input_type: inputType } : {}), ...(queryInputType ? { query_input_type: queryInputType } : {}), ...(truncate ? { truncate } : {}), + ...(dimensions !== undefined ? { dimensions } : {}), ...(maxInputTokens !== undefined ? { max_input_tokens: maxInputTokens } : {}), }; }