Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/web/src/app/api/openrouter/embeddings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
import { emitApiMetricsForResponse } from '@/lib/ai-gateway/o11y/api-metrics.server';
import { normalizeModelId } from '@/lib/ai-gateway/model-utils';
import {
buildDownstreamResponse,
buildUpstreamBody,
type EmbeddingProxyRequest,
} from '@/lib/ai-gateway/embeddings/embedding-request';
Expand Down Expand Up @@ -251,11 +252,15 @@ export async function POST(request: NextRequest): Promise<NextResponseType<unkno
};
}

const response = await embeddingProxyRequest({
const upstreamResponse = await embeddingProxyRequest({
body: upstreamBody,
provider: effectiveProvider,
signal: request.signal,
});
const response = await buildDownstreamResponse(
upstreamResponse,
requestBodyParsed.encoding_format
);

const ttfbMs = Math.max(0, Math.round(performance.now() - requestStartedAt));
usageContext.ttfb_ms = ttfbMs;
Expand Down
51 changes: 48 additions & 3 deletions apps/web/src/lib/ai-gateway/embeddings/embedding-request.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from '@jest/globals';
import { buildUpstreamBody } from './embedding-request';
import { buildDownstreamResponse, buildUpstreamBody } from './embedding-request';

describe('buildUpstreamBody', () => {
it('should forward supported fields and strip client-only, Mistral-specific, and deprecated fields', () => {
it('should forward supported fields and strip client-only, SDK-only, Mistral-specific, and deprecated fields', () => {
const result = buildUpstreamBody({
model: 'google/text-embedding-004',
input: ['text1', 'text2'],
Expand All @@ -18,12 +18,12 @@ describe('buildUpstreamBody', () => {
expect(result).toEqual({
model: 'google/text-embedding-004',
input: ['text1', 'text2'],
encoding_format: 'float',
safety_identifier: 'hash-abc',
provider: { order: ['Google'] },
input_type: 'search_document',
});
expect(result).not.toHaveProperty('dimensions');
expect(result).not.toHaveProperty('encoding_format');
expect(result).not.toHaveProperty('output_dtype');
expect(result).not.toHaveProperty('output_dimension');
});
Expand Down Expand Up @@ -61,3 +61,48 @@ describe('buildUpstreamBody', () => {
expect(result).not.toHaveProperty('output_dimension');
});
});

describe('buildDownstreamResponse', () => {
it('converts numeric embeddings to base64 when the client requested base64', async () => {
const response = new Response(
JSON.stringify({
data: [
{ object: 'embedding', embedding: [1, 2, 3], index: 0 },
{ object: 'embedding', embedding: [4, 5, 6], index: 1 },
],
usage: { prompt_tokens: 1, total_tokens: 1 },
}),
{ status: 200, headers: { 'content-type': 'application/json' } }
);

const result = await buildDownstreamResponse(response, 'base64');
const body = await result.json();

expect(body.data[0].embedding).toBe(
Buffer.from(new Float32Array([1, 2, 3]).buffer).toString('base64')
);
expect(body.data[1].embedding).toBe(
Buffer.from(new Float32Array([4, 5, 6]).buffer).toString('base64')
);
expect(body.usage).toEqual({ prompt_tokens: 1, total_tokens: 1 });
});

it('leaves responses unchanged when base64 was not requested', async () => {
const response = new Response(JSON.stringify({ data: [{ embedding: [1, 2, 3] }] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});

const result = await buildDownstreamResponse(response, undefined);

expect(result).toBe(response);
});

it('leaves error responses unchanged', async () => {
const response = new Response('Bad Request', { status: 400 });

const result = await buildDownstreamResponse(response, 'base64');

expect(result).toBe(response);
});
});
51 changes: 48 additions & 3 deletions apps/web/src/lib/ai-gateway/embeddings/embedding-request.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Buffer } from 'node:buffer';

export type EmbeddingProxyRequest = {
model: string;
input: unknown;
encoding_format?: string;
dimensions?: number;
safety_identifier?: string;
provider?: Record<string, unknown>;
providerOptions?: Record<string, unknown>;
input_type?: string;
// Mistral-specific
output_dtype?: string;
Expand All @@ -21,10 +24,52 @@ export function buildUpstreamBody(
): Record<string, unknown> {
const {
dimensions: _,
output_dtype: __,
output_dimension: ___,
user: ____,
encoding_format: __,
output_dtype: ___,
output_dimension: ____,
user: _____,
...upstreamBody
} = body;
return upstreamBody;
}

function floatEmbeddingToBase64(embedding: number[]): string {
return Buffer.from(new Float32Array(embedding).buffer).toString('base64');
}

export async function buildDownstreamResponse(
response: Response,
encodingFormat: EmbeddingProxyRequest['encoding_format']
): Promise<Response> {
if (!response.ok || encodingFormat !== 'base64') return response;

const contentType = response.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) return response;

const body = await response.clone().json();
Comment thread
marius-kilocode marked this conversation as resolved.
Outdated
if (!body || typeof body !== 'object' || !Array.isArray(body.data)) return response;

const data = body.data.map((item: unknown) => {
if (
!item ||
typeof item !== 'object' ||
!Array.isArray((item as { embedding?: unknown }).embedding)
Comment thread
marius-kilocode marked this conversation as resolved.
Outdated
) {
return item;
}
return {
...item,
embedding: floatEmbeddingToBase64((item as { embedding: number[] }).embedding),
};
});

const headers = new Headers(response.headers);
headers.set('content-type', 'application/json');
headers.delete('content-length');

return new Response(JSON.stringify({ ...body, data }), {
status: response.status,
statusText: response.statusText,
headers,
});
}