Revert "fix: switch embedding dimension from 1536 to 2560 for qwen-embedding-4b"
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 4s

This reverts commit e09ea3a145.
This commit is contained in:
Antigravity
2026-05-12 09:19:01 +00:00
parent e09ea3a145
commit 4c1359ee39
10 changed files with 27 additions and 105 deletions

View File

@@ -131,7 +131,7 @@ model Note {
model NoteEmbedding { model NoteEmbedding {
id String @id @default(cuid()) id String @id @default(cuid())
noteId String @unique noteId String @unique
embedding Unsupported("vector(2560)") embedding Unsupported("vector(1536)")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)

View File

@@ -35,7 +35,7 @@ export async function GET() {
const invalidResult: Array<{ count: bigint }> = await prisma.$queryRawUnsafe( const invalidResult: Array<{ count: bigint }> = await prisma.$queryRawUnsafe(
`SELECT COUNT(*)::bigint as count FROM "NoteEmbedding" e `SELECT COUNT(*)::bigint as count FROM "NoteEmbedding" e
WHERE e."embedding" IS NULL WHERE e."embedding" IS NULL
OR array_length(string_to_array(replace(replace(e."embedding"::text, '[', ''), ']', ''), ','), 1) != 2560` OR array_length(string_to_array(replace(replace(e."embedding"::text, '[', ''), ']', ''), ','), 1) != 1536`
) )
const invalidCount = Number(invalidResult[0]?.count ?? 0) const invalidCount = Number(invalidResult[0]?.count ?? 0)

View File

@@ -1,13 +1,13 @@
import { createOpenAI } from '@ai-sdk/openai'; import { createOpenAI } from '@ai-sdk/openai';
import { generateObject, generateText as aiGenerateText, stepCountIs } from 'ai'; import { generateObject, generateText as aiGenerateText, embed, stepCountIs } from 'ai';
import { z } from 'zod'; import { z } from 'zod';
import { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types'; import { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types';
export class CustomOpenAIProvider implements AIProvider { export class CustomOpenAIProvider implements AIProvider {
private model: any; private model: any;
private embeddingModel: any;
private apiKey: string; private apiKey: string;
private baseUrl: string; private baseUrl: string;
private embeddingModelName: string;
constructor( constructor(
apiKey: string, apiKey: string,
@@ -17,7 +17,6 @@ export class CustomOpenAIProvider implements AIProvider {
) { ) {
this.apiKey = apiKey; this.apiKey = apiKey;
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
this.embeddingModelName = embeddingModelName;
// Create OpenAI-compatible client with custom base URL // Create OpenAI-compatible client with custom base URL
// Use .chat() to force /chat/completions endpoint (avoids Responses API) // Use .chat() to force /chat/completions endpoint (avoids Responses API)
const customClient = createOpenAI({ const customClient = createOpenAI({
@@ -45,16 +44,7 @@ export class CustomOpenAIProvider implements AIProvider {
}); });
this.model = customClient.chat(modelName); this.model = customClient.chat(modelName);
} this.embeddingModel = customClient.embedding(embeddingModelName);
private async fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number = 60_000): Promise<Response> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
return await fetch(url, { ...options, signal: controller.signal })
} finally {
clearTimeout(timer)
}
} }
async generateTags(content: string): Promise<TagSuggestion[]> { async generateTags(content: string): Promise<TagSuggestion[]> {
@@ -80,40 +70,13 @@ export class CustomOpenAIProvider implements AIProvider {
async getEmbeddings(text: string): Promise<number[]> { async getEmbeddings(text: string): Promise<number[]> {
try { try {
const response = await this.fetchWithTimeout(`${this.baseUrl}/embeddings`, { const { embedding } = await embed({
method: 'POST', model: this.embeddingModel,
headers: { value: text,
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'HTTP-Referer': 'https://localhost:3000',
'X-Title': 'Memento AI',
},
body: JSON.stringify({
model: this.embeddingModelName,
input: text,
}),
}); });
return embedding;
if (!response.ok) {
const errText = await response.text();
throw new Error(`${this.baseUrl}/embeddings error ${response.status}: ${errText}`);
}
const data = await response.json();
// Standard OpenAI-compatible response: { data: [{ embedding: number[] }] }
if (data.data && Array.isArray(data.data) && data.data[0]?.embedding) {
return data.data[0].embedding;
}
// Fallback: some providers return { embedding: number[] }
if (data.embedding && Array.isArray(data.embedding)) {
return data.embedding;
}
throw new Error(`Unexpected embeddings response shape: ${JSON.stringify(data)}`);
} catch (e) { } catch (e) {
console.error('Error generating embeddings (CustomOpenAI):', e); console.error('Error generating embeddings (Custom OpenAI):', e);
throw e; throw e;
} }
} }

View File

@@ -1,35 +1,21 @@
import { createOpenAI } from '@ai-sdk/openai'; import { createOpenAI } from '@ai-sdk/openai';
import { generateObject, generateText as aiGenerateText, stepCountIs } from 'ai'; import { generateObject, generateText as aiGenerateText, embed, stepCountIs } from 'ai';
import { z } from 'zod'; import { z } from 'zod';
import { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types'; import { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types';
export class OpenRouterProvider implements AIProvider { export class OpenRouterProvider implements AIProvider {
private model: any; private model: any;
private apiKey: string; private embeddingModel: any;
private baseUrl: string;
private embeddingModelName: string;
constructor(apiKey: string, modelName: string = 'anthropic/claude-3-haiku', embeddingModelName: string = 'openai/text-embedding-3-small') { constructor(apiKey: string, modelName: string = 'anthropic/claude-3-haiku', embeddingModelName: string = 'openai/text-embedding-3-small') {
this.apiKey = apiKey;
this.baseUrl = 'https://openrouter.ai/api/v1';
this.embeddingModelName = embeddingModelName;
// Create OpenAI-compatible client for OpenRouter // Create OpenAI-compatible client for OpenRouter
const openrouter = createOpenAI({ const openrouter = createOpenAI({
baseURL: this.baseUrl, baseURL: 'https://openrouter.ai/api/v1',
apiKey: apiKey, apiKey: apiKey,
}); });
this.model = openrouter.chat(modelName); this.model = openrouter.chat(modelName);
} this.embeddingModel = openrouter.embedding(embeddingModelName);
private async fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number = 60_000): Promise<Response> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeoutMs)
try {
return await fetch(url, { ...options, signal: controller.signal })
} finally {
clearTimeout(timer)
}
} }
async generateTags(content: string): Promise<TagSuggestion[]> { async generateTags(content: string): Promise<TagSuggestion[]> {
@@ -55,38 +41,11 @@ export class OpenRouterProvider implements AIProvider {
async getEmbeddings(text: string): Promise<number[]> { async getEmbeddings(text: string): Promise<number[]> {
try { try {
const response = await this.fetchWithTimeout(`${this.baseUrl}/embeddings`, { const { embedding } = await embed({
method: 'POST', model: this.embeddingModel,
headers: { value: text,
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
'HTTP-Referer': 'https://localhost:3000',
'X-Title': 'Memento AI',
},
body: JSON.stringify({
model: this.embeddingModelName,
input: text,
}),
}); });
return embedding;
if (!response.ok) {
const errText = await response.text();
throw new Error(`OpenRouter embeddings error ${response.status}: ${errText}`);
}
const data = await response.json();
// OpenRouter returns { data: [{ embedding: number[] }] }
if (data.data && Array.isArray(data.data) && data.data[0]?.embedding) {
return data.data[0].embedding;
}
// Fallback: some OpenAI-compatible providers return { embedding: number[] }
if (data.embedding && Array.isArray(data.embedding)) {
return data.embedding;
}
throw new Error(`Unexpected OpenRouter embeddings response shape: ${JSON.stringify(data)}`);
} catch (e) { } catch (e) {
console.error('Error generating embeddings (OpenRouter):', e); console.error('Error generating embeddings (OpenRouter):', e);
throw e; throw e;

View File

@@ -1,7 +1,7 @@
/** /**
* Embedding Service * Embedding Service
* Generates vector embeddings for semantic search and similarity analysis. * Generates vector embeddings for semantic search and similarity analysis.
* Stores embeddings as native pgvector(2560) in PostgreSQL. * Stores embeddings as native pgvector(1536) in PostgreSQL.
*/ */
import { getAIProvider } from '../factory' import { getAIProvider } from '../factory'
@@ -14,7 +14,7 @@ export interface EmbeddingResult {
} }
export class EmbeddingService { export class EmbeddingService {
private readonly EMBEDDING_DIMENSION = 2560 private readonly EMBEDDING_DIMENSION = 1536
async generateEmbedding(text: string): Promise<EmbeddingResult> { async generateEmbedding(text: string): Promise<EmbeddingResult> {
if (!text || text.trim().length === 0) { if (!text || text.trim().length === 0) {

View File

@@ -1,7 +1,7 @@
-- Phase 1: Enable pgvector extension -- Phase 1: Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vector;
-- Phase 2: Convert embedding column from text to vector(2560) -- Phase 2: Convert embedding column from text to vector(1536)
-- Idempotent: detects current column type and only converts when needed. -- Idempotent: detects current column type and only converts when needed.
-- Handles all partial states from previous failed migration attempts: -- Handles all partial states from previous failed migration attempts:
-- A) embedding is text → direct ALTER COLUMN TYPE conversion -- A) embedding is text → direct ALTER COLUMN TYPE conversion
@@ -25,8 +25,8 @@ BEGIN
IF _emb_type IS NOT NULL THEN IF _emb_type IS NOT NULL THEN
ALTER TABLE "NoteEmbedding" DROP COLUMN IF EXISTS "_vec_tmp"; ALTER TABLE "NoteEmbedding" DROP COLUMN IF EXISTS "_vec_tmp";
ALTER TABLE "NoteEmbedding" ALTER TABLE "NoteEmbedding"
ALTER COLUMN "embedding" TYPE vector(2560) ALTER COLUMN "embedding" TYPE vector(1536)
USING "embedding"::vector(2560); USING "embedding"::vector(1536);
RETURN; RETURN;
END IF; END IF;

View File

@@ -300,7 +300,7 @@ model UserAISettings {
model NoteEmbedding { model NoteEmbedding {
id String @id @default(cuid()) id String @id @default(cuid())
noteId String @unique noteId String @unique
embedding Unsupported("vector(2560)") embedding Unsupported("vector(1536)")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)

View File

@@ -41,7 +41,7 @@ async function main() {
// Embedding will be generated by the indexNote method which handles pgvector format // Embedding will be generated by the indexNote method which handles pgvector format
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt") `INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, '[0]'::vector(2560), now(), now()) VALUES (gen_random_uuid(), $1, '[0]'::vector(1536), now(), now())
ON CONFLICT ("noteId") DO NOTHING`, ON CONFLICT ("noteId") DO NOTHING`,
note.id note.id
) )

View File

@@ -47,7 +47,7 @@ test.describe('AI Provider Configuration Tests', () => {
// Verify embeddings provider is working // Verify embeddings provider is working
expect(result.embeddingsProvider).toBe('openai'); expect(result.embeddingsProvider).toBe('openai');
expect(result.embeddingLength).toBe(2560); // OpenAI text-embedding-3-small expect(result.embeddingLength).toBe(1536); // OpenAI text-embedding-3-small
expect(result.details?.provider).toBe('OpenAI'); expect(result.details?.provider).toBe('OpenAI');
}); });

View File

@@ -232,7 +232,7 @@ describe('Data Integrity Tests', () => {
const vecStr = '[0.1,0.2,0.3,0.4,0.5]' const vecStr = '[0.1,0.2,0.3,0.4,0.5]'
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt") `INSERT INTO "NoteEmbedding" ("id", "noteId", "embedding", "createdAt", "updatedAt")
VALUES (gen_random_uuid(), $1, $2::vector(2560), now(), now())`, VALUES (gen_random_uuid(), $1, $2::vector(1536), now(), now())`,
note.id, note.id,
vecStr vecStr
) )