From 9d34098a5342b6f350f45695cf1a9363fad6f139 Mon Sep 17 00:00:00 2001 From: suyao Date: Fri, 28 Nov 2025 13:36:29 +0800 Subject: [PATCH] feat: enhance provider configuration and error handling for AI SDK integration --- packages/shared/provider/constant.ts | 26 +++++++ packages/shared/provider/sdk-config.ts | 12 +++- src/main/apiServer/services/messages.ts | 37 +++++++++- .../apiServer/services/unified-messages.ts | 70 ++++++++++++++++++- src/renderer/src/aiCore/provider/constants.ts | 26 +------ 5 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 packages/shared/provider/constant.ts diff --git a/packages/shared/provider/constant.ts b/packages/shared/provider/constant.ts new file mode 100644 index 0000000000..fe47d6dcce --- /dev/null +++ b/packages/shared/provider/constant.ts @@ -0,0 +1,26 @@ +import { getLowerBaseModelName } from '@shared/utils/naming' + +import type { MinimalModel } from './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: M): boolean { + const normalizedId = getLowerBaseModelName(model.id) + return COPILOT_RESPONSES_MODEL_IDS.some((target) => normalizedId === target) +} diff --git a/packages/shared/provider/sdk-config.ts b/packages/shared/provider/sdk-config.ts index a03b3b1417..e520cb6350 100644 --- a/packages/shared/provider/sdk-config.ts +++ b/packages/shared/provider/sdk-config.ts @@ -127,7 +127,7 @@ export function providerToAiSdkConfig( if (provider.id === SystemProviderIds.copilot) { const defaultHeaders = context.getCopilotDefaultHeaders?.() ?? {} const storedHeaders = context.getCopilotStoredHeaders?.() ?? {} - const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, { + const copilotExtraOptions: Record = { headers: { ...defaultHeaders, ...storedHeaders, @@ -135,7 +135,15 @@ export function providerToAiSdkConfig( }, name: provider.id, includeUsage: true - }) + } + if (context.fetch) { + copilotExtraOptions.fetch = context.fetch + } + const options = ProviderConfigFactory.fromProvider( + 'github-copilot-openai-compatible', + baseConfig, + copilotExtraOptions + ) return { providerId: 'github-copilot-openai-compatible', diff --git a/src/main/apiServer/services/messages.ts b/src/main/apiServer/services/messages.ts index 3277378266..e2c9ad24e2 100644 --- a/src/main/apiServer/services/messages.ts +++ b/src/main/apiServer/services/messages.ts @@ -4,7 +4,7 @@ import { loggerService } from '@logger' import anthropicService from '@main/services/AnthropicService' import { buildClaudeCodeSystemMessage, getSdkClient } from '@shared/anthropic' import type { Provider } from '@types' -import { APICallError } from 'ai' +import { APICallError, RetryError } from 'ai' import { net } from 'electron' import type { Response } from 'express' @@ -267,6 +267,41 @@ export class MessagesService { 500: 'internal_server_error' } + // Handle AI SDK RetryError - extract the last error for better error messages + if (RetryError.isInstance(error)) { + const lastError = error.lastError + // If the last error is an APICallError, extract its details + if (APICallError.isInstance(lastError)) { + statusCode = lastError.statusCode || 502 + errorMessage = lastError.message + return { + statusCode, + errorResponse: { + type: 'error', + error: { + type: errorMap[statusCode] || 'api_error', + message: `${error.reason}: ${errorMessage}`, + requestId: lastError.name + } + } + } + } + // Fallback for other retry errors + errorMessage = error.message + statusCode = 502 + return { + statusCode, + errorResponse: { + type: 'error', + error: { + type: 'api_error', + message: errorMessage, + requestId: error.name + } + } + } + } + if (APICallError.isInstance(error)) { statusCode = error.statusCode errorMessage = error.message diff --git a/src/main/apiServer/services/unified-messages.ts b/src/main/apiServer/services/unified-messages.ts index 5cd59377f6..51751202dd 100644 --- a/src/main/apiServer/services/unified-messages.ts +++ b/src/main/apiServer/services/unified-messages.ts @@ -9,6 +9,8 @@ import type { import { type AiPlugin, createExecutor } from '@cherrystudio/ai-core' import { createProvider as createProviderCore } from '@cherrystudio/ai-core/provider' import { loggerService } from '@logger' +import anthropicService from '@main/services/AnthropicService' +import copilotService from '@main/services/CopilotService' import { reduxService } from '@main/services/ReduxService' import { AiSdkToAnthropicSSE, formatSSEDone, formatSSEEvent } from '@shared/adapters' import { isGemini3ModelId } from '@shared/middleware' @@ -21,6 +23,7 @@ import { providerToAiSdkConfig as sharedProviderToAiSdkConfig, resolveActualProvider } from '@shared/provider' +import { COPILOT_DEFAULT_HEADERS } from '@shared/provider/constant' import { defaultAppHeaders } from '@shared/utils' import type { Provider } from '@types' import type { ImagePart, JSONValue, ModelMessage, Provider as AiSdkProvider, TextPart, Tool } from 'ai' @@ -284,6 +287,68 @@ async function createAiSdkProvider(config: AiSdkConfig): Promise return provider } +/** + * Prepare special provider configuration for providers that need dynamic tokens + * Similar to renderer's prepareSpecialProviderConfig + */ +async function prepareSpecialProviderConfig(provider: Provider, config: AiSdkConfig): Promise { + switch (provider.id) { + case 'copilot': { + const storedHeaders = + ((await reduxService.select('state.copilot.defaultHeaders')) as Record | null) ?? {} + const headers: Record = { + ...COPILOT_DEFAULT_HEADERS, + ...storedHeaders + } + + try { + const { token } = await copilotService.getToken(null as any, headers) + config.options.apiKey = token + const existingHeaders = (config.options.headers as Record | undefined) ?? {} + config.options.headers = { + ...headers, + ...existingHeaders + } + logger.debug('Copilot token retrieved successfully') + } catch (error) { + logger.error('Failed to get Copilot token', error as Error) + throw new Error('Failed to get Copilot token. Please re-authorize Copilot.') + } + break + } + case 'anthropic': { + if (provider.authType === 'oauth') { + try { + const oauthToken = await anthropicService.getValidAccessToken() + if (!oauthToken) { + throw new Error('Anthropic OAuth token not available. Please re-authorize.') + } + config.options = { + ...config.options, + headers: { + ...(config.options.headers ? config.options.headers : {}), + 'Content-Type': 'application/json', + 'anthropic-version': '2023-06-01', + 'anthropic-beta': 'oauth-2025-04-20', + Authorization: `Bearer ${oauthToken}` + }, + baseURL: 'https://api.anthropic.com/v1', + apiKey: '' + } + logger.debug('Anthropic OAuth token retrieved successfully') + } catch (error) { + logger.error('Failed to get Anthropic OAuth token', error as Error) + throw new Error('Failed to get Anthropic OAuth token. Please re-authorize.') + } + } + break + } + // Note: cherryai requires request-level signing which is not easily supported here + // It would need custom fetch implementation similar to renderer + } + return config +} + /** * Core stream execution function - single source of truth for AI SDK calls */ @@ -291,7 +356,10 @@ async function executeStream(config: ExecuteStreamConfig): Promise normalizedId === target || normalizedName === target) -} +export { COPILOT_DEFAULT_HEADERS, isCopilotResponsesModel } from '@shared/provider/constant'