fix: support gpt-5-codex for github copilot (#10587)

* fix: support gpt-5-codex for github copilot

- Added patch for @ai-sdk/openai to version 2.0.42 in package.json and yarn.lock.
- Updated editor version for Copilot from v1.97.2 to v1.104.1 in OpenAIBaseClient and providerConfig.
- Enhanced provider configuration to support new model options for Copilot.

* fix: streamline Copilot header management

- Replaced individual header assignments for Copilot with centralized constants in OpenAIBaseClient and providerConfig.
- Enhanced provider configuration to conditionally set response mode for Copilot models, improving routing logic.

* update aisdk

* delete patch

* 🤖 chore: integrate Copilot SDK provider

* use a plugin

* udpate dependency

* fix: remove unused Copilot default headers from OpenAIBaseClient

- Eliminated the import and usage of COPILOT_DEFAULT_HEADERS to streamline header management in the OpenAIBaseClient class.

* update yarn

* fix lint

* format code

* feat: enhance web search tool types in webSearchPlugin

- Added type normalization for web search tools to improve type safety and clarity.
- Updated WebSearchToolInputSchema and WebSearchToolOutputSchema to use normalized types for better consistency across the plugin.
This commit is contained in:
beyondkmp 2025-10-10 19:18:09 -07:00 committed by GitHub
parent acdbe6b9ed
commit 1c73271e33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 221 additions and 23 deletions

View File

@ -152,6 +152,7 @@
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-node": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@opeoginni/github-copilot-openai-compatible": "0.1.18",
"@playwright/test": "^1.52.0",
"@radix-ui/react-context-menu": "^2.2.16",
"@reduxjs/toolkit": "^2.2.5",

View File

@ -1,7 +1,7 @@
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai'
import { InferToolInput, InferToolOutput } from 'ai'
import { InferToolInput, InferToolOutput, type Tool } from 'ai'
import { ProviderOptionsMap } from '../../../options/types'
import { OpenRouterSearchConfig } from './openrouter'
@ -15,6 +15,13 @@ export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tool
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
type NormalizeTool<T> = T extends Tool<infer INPUT, infer OUTPUT> ? Tool<INPUT, OUTPUT> : Tool<any, any>
type AnthropicWebSearchTool = NormalizeTool<ReturnType<typeof anthropic.tools.webSearch_20250305>>
type OpenAIWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearch>>
type OpenAIChatWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearchPreview>>
type GoogleWebSearchTool = NormalizeTool<ReturnType<typeof google.tools.googleSearch>>
/**
*
*
@ -59,7 +66,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
export type WebSearchToolOutputSchema = {
// Anthropic 工具 - 手动定义
anthropic: InferToolOutput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
anthropic: InferToolOutput<AnthropicWebSearchTool>
// OpenAI 工具 - 基于实际输出
// TODO: 上游定义不规范,是unknown
@ -82,8 +89,8 @@ export type WebSearchToolOutputSchema = {
}
export type WebSearchToolInputSchema = {
anthropic: InferToolInput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
openai: InferToolInput<ReturnType<typeof openai.tools.webSearch>>
google: InferToolInput<ReturnType<typeof google.tools.googleSearch>>
'openai-chat': InferToolInput<ReturnType<typeof openai.tools.webSearchPreview>>
anthropic: InferToolInput<AnthropicWebSearchTool>
openai: InferToolInput<OpenAIWebSearchTool>
google: InferToolInput<GoogleWebSearchTool>
'openai-chat': InferToolInput<OpenAIChatWebSearchTool>
}

View File

@ -166,9 +166,7 @@ export abstract class OpenAIBaseClient<
baseURL: this.getBaseURL(),
defaultHeaders: {
...this.defaultHeaders(),
...this.provider.extra_headers,
...(this.provider.id === 'copilot' ? { 'editor-version': 'vscode/1.97.2' } : {}),
...(this.provider.id === 'copilot' ? { 'copilot-vision-request': 'true' } : {})
...this.provider.extra_headers
}
}) as TSdkInstance
}

View File

@ -0,0 +1,89 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@renderer/services/LoggerService', () => ({
loggerService: {
withContext: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
})
}
}))
vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn()
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({ copilot: { defaultHeaders: {} } })
}
}))
import type { Model, Provider } from '@renderer/types'
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
import { providerToAiSdkConfig } from '../providerConfig'
const createWindowKeyv = () => {
const store = new Map<string, string>()
return {
get: (key: string) => store.get(key),
set: (key: string, value: string) => {
store.set(key, value)
}
}
}
const createCopilotProvider = (): Provider => ({
id: 'copilot',
type: 'openai',
name: 'GitHub Copilot',
apiKey: 'test-key',
apiHost: 'https://api.githubcopilot.com',
models: [],
isSystem: true
})
const createModel = (id: string, name = id): Model => ({
id,
name,
provider: 'copilot',
group: 'copilot'
})
describe('Copilot responses routing', () => {
beforeEach(() => {
;(globalThis as any).window = {
...(globalThis as any).window,
keyv: createWindowKeyv()
}
})
it('detects official GPT-5 Codex identifiers case-insensitively', () => {
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'gpt-5-codex'))).toBe(true)
expect(isCopilotResponsesModel(createModel('GPT-5-CODEX', 'GPT-5-CODEX'))).toBe(true)
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'custom-name'))).toBe(true)
expect(isCopilotResponsesModel(createModel('custom-id', 'custom-name'))).toBe(false)
})
it('configures gpt-5-codex with the Copilot provider', () => {
const provider = createCopilotProvider()
const config = providerToAiSdkConfig(provider, createModel('gpt-5-codex', 'GPT-5-CODEX'))
expect(config.providerId).toBe('github-copilot-openai-compatible')
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_EDITOR_VERSION)
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
expect(config.options.headers?.['copilot-vision-request']).toBe('true')
})
it('uses the Copilot provider for other models and keeps headers', () => {
const provider = createCopilotProvider()
const config = providerToAiSdkConfig(provider, createModel('gpt-4'))
expect(config.providerId).toBe('github-copilot-openai-compatible')
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_DEFAULT_HEADERS['Editor-Version'])
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
})
})

View File

@ -0,0 +1,25 @@
import type { Model } from '@renderer/types'
export const COPILOT_EDITOR_VERSION = 'vscode/1.104.1'
export const COPILOT_PLUGIN_VERSION = 'copilot-chat/0.26.7'
export const COPILOT_INTEGRATION_ID = 'vscode-chat'
export const COPILOT_USER_AGENT = 'GitHubCopilotChat/0.26.7'
export const COPILOT_DEFAULT_HEADERS = {
'Copilot-Integration-Id': COPILOT_INTEGRATION_ID,
'User-Agent': COPILOT_USER_AGENT,
'Editor-Version': COPILOT_EDITOR_VERSION,
'Editor-Plugin-Version': COPILOT_PLUGIN_VERSION,
'editor-version': COPILOT_EDITOR_VERSION,
'editor-plugin-version': COPILOT_PLUGIN_VERSION,
'copilot-vision-request': 'true'
} as const
// Models that require the OpenAI Responses endpoint when routed through GitHub Copilot (#10560)
const COPILOT_RESPONSES_MODEL_IDS = ['gpt-5-codex']
export function isCopilotResponsesModel(model: Model): boolean {
const normalizedId = model.id?.trim().toLowerCase()
const normalizedName = model.name?.trim().toLowerCase()
return COPILOT_RESPONSES_MODEL_IDS.some((target) => normalizedId === target || normalizedName === target)
}

View File

@ -28,7 +28,8 @@ const STATIC_PROVIDER_MAPPING: Record<string, ProviderId> = {
gemini: 'google', // Google Gemini -> google
'azure-openai': 'azure', // Azure OpenAI -> azure
'openai-response': 'openai', // OpenAI Responses -> openai
grok: 'xai' // Grok -> xai
grok: 'xai', // Grok -> xai
copilot: 'github-copilot-openai-compatible'
}
/**

View File

@ -21,6 +21,7 @@ import { formatApiHost } from '@renderer/utils/api'
import { cloneDeep, trim } from 'lodash'
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
import { COPILOT_DEFAULT_HEADERS } from './constants'
import { getAiSdkProviderId } from './factory'
const logger = loggerService.withContext('ProviderConfigProcessor')
@ -109,6 +110,9 @@ function formatProviderApiHost(provider: Provider): Provider {
if (!formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatted.apiHost
}
} else if (formatted.id === 'copilot') {
const trimmed = trim(formatted.apiHost)
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
} else if (formatted.type === 'gemini') {
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
} else {
@ -151,6 +155,26 @@ export function providerToAiSdkConfig(
baseURL: trim(actualProvider.apiHost),
apiKey: getRotatedApiKey(actualProvider)
}
const isCopilotProvider = actualProvider.id === 'copilot'
if (isCopilotProvider) {
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
headers: {
...COPILOT_DEFAULT_HEADERS,
...storedHeaders,
...actualProvider.extra_headers
},
name: actualProvider.id,
includeUsage: true
})
return {
providerId: 'github-copilot-openai-compatible',
options
}
}
// 处理OpenAI模式
const extraOptions: any = {}
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
@ -172,15 +196,6 @@ export function providerToAiSdkConfig(
}
}
}
// copilot
if (actualProvider.id === 'copilot') {
extraOptions.headers = {
...extraOptions.headers,
'editor-version': 'vscode/1.97.2',
'copilot-vision-request': 'true'
}
}
// azure
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
extraOptions.apiVersion = actualProvider.apiVersion
@ -229,7 +244,6 @@ export function providerToAiSdkConfig(
}
}
// 如果AI SDK支持该provider使用原生配置
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
const options = ProviderConfigFactory.fromProvider(aiSdkProviderId, baseConfig, extraOptions)
return {
@ -277,9 +291,17 @@ export async function prepareSpecialProviderConfig(
) {
switch (provider.id) {
case 'copilot': {
const defaultHeaders = store.getState().copilot.defaultHeaders
const { token } = await window.api.copilot.getToken(defaultHeaders)
const defaultHeaders = store.getState().copilot.defaultHeaders ?? {}
const headers = {
...COPILOT_DEFAULT_HEADERS,
...defaultHeaders
}
const { token } = await window.api.copilot.getToken(headers)
config.options.apiKey = token
config.options.headers = {
...headers,
...config.options.headers
}
break
}
case 'cherryai': {

View File

@ -32,6 +32,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
supportsImageGeneration: true,
aliases: ['vertexai-anthropic']
},
{
id: 'github-copilot-openai-compatible',
name: 'GitHub Copilot OpenAI Compatible',
import: () => import('@opeoginni/github-copilot-openai-compatible'),
creatorFunctionName: 'createGitHubCopilotOpenAICompatible',
supportsImageGeneration: false,
aliases: ['copilot', 'github-copilot']
},
{
id: 'bedrock',
name: 'Amazon Bedrock',

View File

@ -191,7 +191,7 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai@npm:2.0.42, @ai-sdk/openai@npm:^2.0.42":
"@ai-sdk/openai@npm:2.0.42":
version: 2.0.42
resolution: "@ai-sdk/openai@npm:2.0.42"
dependencies:
@ -203,6 +203,18 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai@npm:^2.0.42":
version: 2.0.47
resolution: "@ai-sdk/openai@npm:2.0.47"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.11"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/7fabcdda707134971bcc2b285705d4595f8bf419285dbdd9266b3b0858ea11b6ac200e63dd2eeb1822f99571910093d64d4a76154a365331cf184f56452933c6
languageName: node
linkType: hard
"@ai-sdk/perplexity@npm:^2.0.11":
version: 2.0.11
resolution: "@ai-sdk/perplexity@npm:2.0.11"
@ -228,6 +240,19 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.11":
version: 3.0.11
resolution: "@ai-sdk/provider-utils@npm:3.0.11"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/31081b127b48f3eefb448eaca59574b4631da9577aa0778622d28669c71bbde0361c9b37962c5edbb1d0c163ed1479755fc889da9251a03e906b1e27d0d2eb24
languageName: node
linkType: hard
"@ai-sdk/provider@npm:2.0.0, @ai-sdk/provider@npm:^2.0.0":
version: 2.0.0
resolution: "@ai-sdk/provider@npm:2.0.0"
@ -237,6 +262,15 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider@npm:^2.1.0-beta.4":
version: 2.1.0-beta.5
resolution: "@ai-sdk/provider@npm:2.1.0-beta.5"
dependencies:
json-schema: "npm:^0.4.0"
checksum: 10c0/4f51813285a8e92be18ef14645b0505bb0c9d2daa0d9290cac8a3c1f87d8e2e8507b1edf1818ae2305a90723e8cd44477f55f0631407dee35912ab8fdded52ba
languageName: node
linkType: hard
"@ai-sdk/xai@npm:^2.0.23":
version: 2.0.23
resolution: "@ai-sdk/xai@npm:2.0.23"
@ -7673,6 +7707,18 @@ __metadata:
languageName: node
linkType: hard
"@opeoginni/github-copilot-openai-compatible@npm:0.1.18":
version: 0.1.18
resolution: "@opeoginni/github-copilot-openai-compatible@npm:0.1.18"
dependencies:
"@ai-sdk/openai": "npm:^2.0.42"
"@ai-sdk/openai-compatible": "npm:^1.0.19"
"@ai-sdk/provider": "npm:^2.1.0-beta.4"
"@ai-sdk/provider-utils": "npm:^3.0.10"
checksum: 10c0/31b87ed150883bbdd33a0203e45831859560fdf174f0285384fdcb1d01fc4a56ca15f31d648e8d6d3a2d4d5c6e327ddecbf422543eeefaa7e8fdd7dc2f2a3b08
languageName: node
linkType: hard
"@oxc-project/runtime@npm:0.71.0":
version: 0.71.0
resolution: "@oxc-project/runtime@npm:0.71.0"
@ -14238,6 +14284,7 @@ __metadata:
"@opentelemetry/sdk-trace-base": "npm:^2.0.0"
"@opentelemetry/sdk-trace-node": "npm:^2.0.0"
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
"@opeoginni/github-copilot-openai-compatible": "npm:0.1.18"
"@playwright/test": "npm:^1.52.0"
"@radix-ui/react-context-menu": "npm:^2.2.16"
"@reduxjs/toolkit": "npm:^2.2.5"