feat(server): adapt gpt5 (#13478)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Added GPT-5 family and made GPT-5/-mini the new defaults for Copilot
scenarios and prompts.

- Bug Fixes
- Improved streaming chunk formats and reasoning/text semantics,
consistent attachment mediaType handling, and more reliable reranking
via log-prob handling.

- Refactor
- Unified maxOutputTokens usage; removed per-call step caps and migrated
several tools to a unified inputSchema shape.

- Chores
- Upgraded AI SDK dependencies and bumped an internal dependency
version.

- Tests
- Updated mocks and tests to reference GPT-5 variants and new stream
formats.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2025-08-13 10:32:15 +08:00
committed by GitHub
parent fda7e9008d
commit 072557eba1
29 changed files with 410 additions and 374 deletions
@@ -10,6 +10,7 @@ import {
experimental_generateImage as generateImage,
generateObject,
generateText,
stepCountIs,
streamText,
Tool,
} from 'ai';
@@ -65,6 +66,18 @@ const ImageResponseSchema = z.union([
}),
}),
]);
const LogProbsSchema = z.array(
z.object({
token: z.string(),
logprob: z.number(),
top_logprobs: z.array(
z.object({
token: z.string(),
logprob: z.number(),
})
),
})
);
export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
readonly type = CopilotProviderType.OpenAI;
@@ -162,6 +175,58 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
},
],
},
{
id: 'gpt-5',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gpt-5-2025-08-07',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gpt-5-mini',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'gpt-5-nano',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [
ModelOutputType.Text,
ModelOutputType.Object,
ModelOutputType.Structured,
],
},
],
},
{
id: 'o1',
capabilities: [
@@ -299,7 +364,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
model: string
): [string, Tool?] | undefined {
if (toolName === 'webSearch' && !this.isReasoningModel(model)) {
return ['web_search_preview', openai.tools.webSearchPreview()];
return ['web_search_preview', openai.tools.webSearchPreview({})];
} else if (toolName === 'docEdit') {
return ['doc_edit', undefined];
}
@@ -330,12 +395,12 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
system,
messages: msgs,
temperature: options.temperature ?? 0,
maxTokens: options.maxTokens ?? 4096,
maxOutputTokens: options.maxTokens ?? 4096,
providerOptions: {
openai: this.getOpenAIOptions(options, model.id),
},
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
stopWhen: stepCountIs(this.MAX_STEPS),
abortSignal: options.signal,
});
@@ -451,7 +516,7 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
system,
messages: msgs,
temperature: options.temperature ?? 0,
maxTokens: options.maxTokens ?? 4096,
maxOutputTokens: options.maxTokens ?? 4096,
maxRetries: options.maxRetries ?? 3,
schema,
providerOptions: {
@@ -476,36 +541,37 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
await this.checkParams({ messages: [], cond: fullCond, options });
const model = this.selectModel(fullCond);
// get the log probability of "yes"/"no"
const instance = this.#instance(model.id, { logprobs: 16 });
const instance = this.#instance.chat(model.id);
const scores = await Promise.all(
chunkMessages.map(async messages => {
const [system, msgs] = await chatToGPTMessage(messages);
const { logprobs } = await generateText({
const result = await generateText({
model: instance,
system,
messages: msgs,
temperature: 0,
maxTokens: 16,
maxOutputTokens: 16,
providerOptions: {
openai: {
...this.getOpenAIOptions(options, model.id),
logprobs: 16,
},
},
abortSignal: options.signal,
});
const topMap: Record<string, number> = (
logprobs?.[0]?.topLogprobs ?? []
).reduce<Record<string, number>>(
const topMap: Record<string, number> = LogProbsSchema.parse(
result.providerMetadata?.openai?.logprobs
)[0].top_logprobs.reduce<Record<string, number>>(
(acc, { token, logprob }) => ({ ...acc, [token]: logprob }),
{}
);
const findLogProb = (token: string): number => {
// OpenAI often includes a leading space, so try matching '.yes', '_yes', ' yes' and 'yes'
return [`.${token}`, `_${token}`, ` ${token}`, token]
return [...'_:. "-\t,(=_“'.split('').map(c => c + token), token]
.flatMap(v => [v, v.toLowerCase(), v.toUpperCase()])
.reduce<number>(
(best, key) =>
@@ -544,12 +610,12 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
frequencyPenalty: options.frequencyPenalty ?? 0,
presencePenalty: options.presencePenalty ?? 0,
temperature: options.temperature ?? 0,
maxTokens: options.maxTokens ?? 4096,
maxOutputTokens: options.maxTokens ?? 4096,
providerOptions: {
openai: this.getOpenAIOptions(options, model.id),
},
tools: await this.getTools(options, model.id),
maxSteps: this.MAX_STEPS,
stopWhen: stepCountIs(this.MAX_STEPS),
abortSignal: options.signal,
});
return fullStream;
@@ -676,14 +742,16 @@ export class OpenAIProvider extends CopilotProvider<OpenAIConfig> {
.counter('generate_embedding_calls')
.add(1, { model: model.id });
const modelInstance = this.#instance.embedding(model.id, {
dimensions: options.dimensions || DEFAULT_DIMENSIONS,
user: options.user,
});
const modelInstance = this.#instance.embedding(model.id);
const { embeddings } = await embedMany({
model: modelInstance,
values: messages,
providerOptions: {
openai: {
dimensions: options.dimensions || DEFAULT_DIMENSIONS,
},
},
});
return embeddings.filter(v => v && Array.isArray(v));