fix(server): rerank scores calc (#13016)

fix AI-257
This commit is contained in:
DarkSky
2025-07-07 23:05:02 +08:00
committed by GitHub
parent 2d050a317f
commit 181ccf5a45
18 changed files with 329 additions and 109 deletions
@@ -5,6 +5,7 @@ import type {
import type { GoogleVertexProvider } from '@ai-sdk/google-vertex';
import {
AISDKError,
embedMany,
generateObject,
generateText,
JSONParseError,
@@ -20,6 +21,7 @@ import {
import { CopilotProvider } from '../provider';
import type {
CopilotChatOptions,
CopilotEmbeddingOptions,
CopilotImageOptions,
CopilotProviderModel,
ModelConditions,
@@ -211,6 +213,40 @@ export abstract class GeminiProvider<T> extends CopilotProvider<T> {
}
}
override async embedding(
cond: ModelConditions,
messages: string | string[],
options: CopilotEmbeddingOptions = { dimensions: DEFAULT_DIMENSIONS }
): Promise<number[][]> {
messages = Array.isArray(messages) ? messages : [messages];
const fullCond = { ...cond, outputType: ModelOutputType.Embedding };
await this.checkParams({ embeddings: messages, cond: fullCond, options });
const model = this.selectModel(fullCond);
try {
metrics.ai
.counter('generate_embedding_calls')
.add(1, { model: model.id });
const modelInstance = this.instance.textEmbeddingModel(model.id, {
outputDimensionality: options.dimensions || DEFAULT_DIMENSIONS,
taskType: 'RETRIEVAL_DOCUMENT',
});
const { embeddings } = await embedMany({
model: modelInstance,
values: messages,
});
return embeddings.filter(v => v && Array.isArray(v));
} catch (e: any) {
metrics.ai
.counter('generate_embedding_errors')
.add(1, { model: model.id });
throw this.handleError(e);
}
}
private async getFullStream(
model: CopilotProviderModel,
messages: PromptMessage[],
@@ -71,8 +71,8 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
],
},
{
name: 'Text Embedding 004',
id: 'text-embedding-004',
name: 'Text Embedding 005',
id: 'text-embedding-005',
capabilities: [
{
input: [ModelInputType.Text],
@@ -80,6 +80,18 @@ export class GeminiGenerativeProvider extends GeminiProvider<GeminiGenerativeCon
},
],
},
// not exists yet
// {
// name: 'Gemini Embedding',
// id: 'gemini-embedding-001',
// capabilities: [
// {
// input: [ModelInputType.Text],
// output: [ModelOutputType.Embedding],
// defaultForOutputType: true,
// },
// ],
// },
];
protected instance!: GoogleGenerativeAIProvider;
@@ -49,6 +49,17 @@ export class GeminiVertexProvider extends GeminiProvider<GeminiVertexConfig> {
},
],
},
{
name: 'Gemini Embedding',
id: 'gemini-embedding-001',
capabilities: [
{
input: [ModelInputType.Text],
output: [ModelOutputType.Embedding],
defaultForOutputType: true,
},
],
},
];
protected instance!: GoogleVertexProvider;
@@ -450,7 +450,8 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
const fullCond = { ...cond, outputType: ModelOutputType.Text };
await this.checkParams({ messages: [], cond: fullCond, options });
const model = this.selectModel(fullCond);
const instance = this.#instance.responses(model.id);
// get the log probability of "yes"/"no"
const instance = this.#instance(model.id, { logprobs: 16 });
const scores = await Promise.all(
chunkMessages.map(async messages => {
@@ -461,29 +462,37 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
system,
messages: msgs,
temperature: 0,
maxTokens: 1,
maxTokens: 16,
providerOptions: {
openai: {
...this.getOpenAIOptions(options, model.id),
// get the log probability of "yes"/"no"
logprobs: 2,
},
},
maxSteps: 1,
abortSignal: options.signal,
});
const top = (logprobs?.[0]?.topLogprobs ?? []).reduce(
(acc, item) => {
acc[item.token] = item.logprob;
return acc;
},
{} as Record<string, number>
const topMap: Record<string, number> = (
logprobs?.[0]?.topLogprobs ?? []
).reduce<Record<string, number>>(
(acc, { token, logprob }) => ({ ...acc, [token]: logprob }),
{}
);
// OpenAI often includes a leading space, so try matching both ' yes' and 'yes'
const logYes = top[' yes'] ?? top['yes'] ?? Number.NEGATIVE_INFINITY;
const logNo = top[' no'] ?? top['no'] ?? Number.NEGATIVE_INFINITY;
const findLogProb = (token: string): number => {
// OpenAI often includes a leading space, so try matching '.yes', '_yes', ' yes' and 'yes'
return [`.${token}`, `_${token}`, ` ${token}`, token]
.flatMap(v => [v, v.toLowerCase(), v.toUpperCase()])
.reduce<number>(
(best, key) =>
(topMap[key] ?? Number.NEGATIVE_INFINITY) > best
? topMap[key]
: best,
Number.NEGATIVE_INFINITY
);
};
const logYes = findLogProb('Yes');
const logNo = findLogProb('No');
const pYes = Math.exp(logYes);
const pNo = Math.exp(logNo);
@@ -501,6 +501,12 @@ export class TextStreamParser {
case 'doc_semantic_search': {
if (Array.isArray(chunk.result)) {
result += `\nFound ${chunk.result.length} document${chunk.result.length !== 1 ? 's' : ''} related to “${chunk.args.query}”.\n`;
} else if (typeof chunk.result === 'string') {
result += `\n${chunk.result}\n`;
} else {
this.logger.warn(
`Unexpected result type for doc_semantic_search: ${chunk.result?.message || 'Unknown error'}`
);
}
break;
}