mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
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:
parent
acdbe6b9ed
commit
1c73271e33
@ -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",
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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'])
|
||||
})
|
||||
})
|
||||
25
src/renderer/src/aiCore/provider/constants.ts
Normal file
25
src/renderer/src/aiCore/provider/constants.ts
Normal 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)
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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': {
|
||||
|
||||
@ -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',
|
||||
|
||||
49
yarn.lock
49
yarn.lock
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user