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

@@ -35,7 +35,7 @@ export async function GET() {
const invalidResult: Array<{ count: bigint }> = await prisma.$queryRawUnsafe(
`SELECT COUNT(*)::bigint as count FROM "NoteEmbedding" e
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)

View File

@@ -1,13 +1,13 @@
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 { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types';
export class CustomOpenAIProvider implements AIProvider {
private model: any;
private embeddingModel: any;
private apiKey: string;
private baseUrl: string;
private embeddingModelName: string;
constructor(
apiKey: string,
@@ -17,7 +17,6 @@ export class CustomOpenAIProvider implements AIProvider {
) {
this.apiKey = apiKey;
this.baseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
this.embeddingModelName = embeddingModelName;
// Create OpenAI-compatible client with custom base URL
// Use .chat() to force /chat/completions endpoint (avoids Responses API)
const customClient = createOpenAI({
@@ -45,16 +44,7 @@ export class CustomOpenAIProvider implements AIProvider {
});
this.model = customClient.chat(modelName);
}
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)
}
this.embeddingModel = customClient.embedding(embeddingModelName);
}
async generateTags(content: string): Promise<TagSuggestion[]> {
@@ -80,40 +70,13 @@ export class CustomOpenAIProvider implements AIProvider {
async getEmbeddings(text: string): Promise<number[]> {
try {
const response = await this.fetchWithTimeout(`${this.baseUrl}/embeddings`, {
method: 'POST',
headers: {
'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,
}),
const { embedding } = await embed({
model: this.embeddingModel,
value: text,
});
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)}`);
return embedding;
} catch (e) {
console.error('Error generating embeddings (CustomOpenAI):', e);
console.error('Error generating embeddings (Custom OpenAI):', e);
throw e;
}
}

View File

@@ -1,35 +1,21 @@
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 { AIProvider, TagSuggestion, TitleSuggestion, ToolUseOptions, ToolCallResult } from '../types';
export class OpenRouterProvider implements AIProvider {
private model: any;
private apiKey: string;
private baseUrl: string;
private embeddingModelName: string;
private embeddingModel: any;
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
const openrouter = createOpenAI({
baseURL: this.baseUrl,
baseURL: 'https://openrouter.ai/api/v1',
apiKey: apiKey,
});
this.model = openrouter.chat(modelName);
}
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)
}
this.embeddingModel = openrouter.embedding(embeddingModelName);
}
async generateTags(content: string): Promise<TagSuggestion[]> {
@@ -55,38 +41,11 @@ export class OpenRouterProvider implements AIProvider {
async getEmbeddings(text: string): Promise<number[]> {
try {
const response = await this.fetchWithTimeout(`${this.baseUrl}/embeddings`, {
method: 'POST',
headers: {
'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,
}),
const { embedding } = await embed({
model: this.embeddingModel,
value: text,
});
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)}`);
return embedding;
} catch (e) {
console.error('Error generating embeddings (OpenRouter):', e);
throw e;

View File

@@ -1,7 +1,7 @@
/**
* Embedding Service
* 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'
@@ -14,7 +14,7 @@ export interface EmbeddingResult {
}
export class EmbeddingService {
private readonly EMBEDDING_DIMENSION = 2560
private readonly EMBEDDING_DIMENSION = 1536
async generateEmbedding(text: string): Promise<EmbeddingResult> {
if (!text || text.trim().length === 0) {

View File

@@ -1,7 +1,7 @@
-- Phase 1: Enable pgvector extension
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.
-- Handles all partial states from previous failed migration attempts:
-- A) embedding is text → direct ALTER COLUMN TYPE conversion
@@ -25,8 +25,8 @@ BEGIN
IF _emb_type IS NOT NULL THEN
ALTER TABLE "NoteEmbedding" DROP COLUMN IF EXISTS "_vec_tmp";
ALTER TABLE "NoteEmbedding"
ALTER COLUMN "embedding" TYPE vector(2560)
USING "embedding"::vector(2560);
ALTER COLUMN "embedding" TYPE vector(1536)
USING "embedding"::vector(1536);
RETURN;
END IF;

View File

@@ -300,7 +300,7 @@ model UserAISettings {
model NoteEmbedding {
id String @id @default(cuid())
noteId String @unique
embedding Unsupported("vector(2560)")
embedding Unsupported("vector(1536)")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
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
await prisma.$executeRawUnsafe(
`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`,
note.id
)

View File

@@ -47,7 +47,7 @@ test.describe('AI Provider Configuration Tests', () => {
// Verify embeddings provider is working
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');
});

View File

@@ -232,7 +232,7 @@ describe('Data Integrity Tests', () => {
const vecStr = '[0.1,0.2,0.3,0.4,0.5]'
await prisma.$executeRawUnsafe(
`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,
vecStr
)