import { getGlobalConfig, saveGlobalConfig, type ConnectedProviderInfo } from '../../utils/config.js' const COPILOT_API_BASE = 'https://api.githubcopilot.com' const MODELS_DEV_URL = 'https://models.dev/api.json' export type CopilotModelInfo = { id: string label: string description: string supportedEndpoints?: string[] } type CopilotOutputTokenParam = 'max_tokens' | 'max_completion_tokens' type CopilotCompatibilityInfo = { outputTokenParam?: CopilotOutputTokenParam modelSupported?: boolean chatCompletionsSupported?: boolean updatedAt: number } const COPILOT_COMPATIBILITY_CACHE_TTL_MS = 24 * 60 * 60 * 1000 const COPILOT_CHAT_COMPLETIONS_ENDPOINT = '/chat/completions' export function isCopilotModel(model: string): boolean { return model.startsWith('copilot:') } export function getCopilotModelId(model: string): string { return model.replace(/^copilot:/, '') } export function getCopilotProvider(): ConnectedProviderInfo | undefined { const config = getGlobalConfig() return config.connectedProviders?.['github-copilot'] } export function isCopilotConnected(): boolean { return !!getCopilotProvider()?.oauthToken } const FALLBACK_COPILOT_MODELS: CopilotModelInfo[] = [ { id: 'copilot:claude-sonnet-4.6', label: 'Claude Sonnet 4.6', description: 'Claude Sonnet 4.6 via Copilot' }, { id: 'copilot:claude-sonnet-4.5', label: 'Claude Sonnet 4.5', description: 'Claude Sonnet 4.5 via Copilot' }, { id: 'copilot:claude-sonnet-4', label: 'Claude Sonnet 4', description: 'Claude Sonnet 4 via Copilot' }, { id: 'copilot:claude-opus-4.6', label: 'Claude Opus 4.6', description: 'Claude Opus 4.6 via Copilot' }, { id: 'copilot:claude-opus-4.5', label: 'Claude Opus 4.5', description: 'Claude Opus 4.5 via Copilot' }, { id: 'copilot:claude-opus-41', label: 'Claude Opus 4.1', description: 'Claude Opus 4.1 via Copilot' }, { id: 'copilot:claude-haiku-4.5', label: 'Claude Haiku 4.5', description: 'Claude Haiku 4.5 via Copilot' }, { id: 'copilot:gpt-5.4', label: 'GPT-5.4', description: 'OpenAI GPT-5.4 via Copilot' }, { id: 'copilot:gpt-5.4-mini', label: 'GPT-5.4 mini', description: 'OpenAI GPT-5.4 mini via Copilot' }, { id: 'copilot:gpt-5.3-codex', label: 'GPT-5.3-Codex', description: 'OpenAI GPT-5.3-Codex via Copilot' }, { id: 'copilot:gpt-5.2-codex', label: 'GPT-5.2-Codex', description: 'OpenAI GPT-5.2-Codex via Copilot' }, { id: 'copilot:gpt-5.2', label: 'GPT-5.2', description: 'OpenAI GPT-5.2 via Copilot' }, { id: 'copilot:gpt-5.1', label: 'GPT-5.1', description: 'OpenAI GPT-5.1 via Copilot' }, { id: 'copilot:gpt-5.1-codex', label: 'GPT-5.1-Codex', description: 'OpenAI GPT-5.1-Codex via Copilot' }, { id: 'copilot:gpt-5.1-codex-mini', label: 'GPT-5.1-Codex-mini', description: 'OpenAI GPT-5.1-Codex-mini via Copilot' }, { id: 'copilot:gpt-5.1-codex-max', label: 'GPT-5.1-Codex-max', description: 'OpenAI GPT-5.1-Codex-max via Copilot' }, { id: 'copilot:gpt-5', label: 'GPT-5', description: 'OpenAI GPT-5 via Copilot' }, { id: 'copilot:gpt-5-mini', label: 'GPT-5-mini', description: 'OpenAI GPT-5-mini via Copilot' }, { id: 'copilot:gpt-4.1', label: 'GPT-4.1', description: 'OpenAI GPT-4.1 via Copilot' }, { id: 'copilot:gpt-4o', label: 'GPT-4o', description: 'OpenAI GPT-4o via Copilot' }, { id: 'copilot:gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview', description: 'Google Gemini 3.1 Pro via Copilot' }, { id: 'copilot:gemini-3-pro-preview', label: 'Gemini 3 Pro Preview', description: 'Google Gemini 3 Pro via Copilot' }, { id: 'copilot:gemini-3-flash-preview', label: 'Gemini 3 Flash', description: 'Google Gemini 3 Flash via Copilot' }, { id: 'copilot:gemini-2.5-pro', label: 'Gemini 2.5 Pro', description: 'Google Gemini 2.5 Pro via Copilot' }, { id: 'copilot:grok-code-fast-1', label: 'Grok Code Fast 1', description: 'xAI Grok Code Fast 1 via Copilot' }, ] let cachedModels: CopilotModelInfo[] | null = null let copilotModelsRefreshPromise: Promise | null = null type CopilotApiModel = { id: string name?: string model_picker_enabled?: boolean policy?: { state?: string } supported_endpoints?: string[] } function supportsCopilotChatCompletions(model: CopilotModelInfo): boolean { return ( !model.supportedEndpoints || model.supportedEndpoints.includes(COPILOT_CHAT_COMPLETIONS_ENDPOINT) ) } export async function fetchCopilotModelsFromApi(): Promise { const provider = getCopilotProvider() if (!provider?.oauthToken) return null const response = await fetch(`${COPILOT_API_BASE}/models`, { headers: { Authorization: `Bearer ${provider.oauthToken}`, 'User-Agent': 'claude-code/2.1.88', 'Openai-Intent': 'conversation-edits', 'x-initiator': 'user', }, signal: AbortSignal.timeout(10_000), }) if (!response.ok) { throw new Error(`Copilot models API failed (${response.status})`) } const data = await response.json() as { data?: CopilotApiModel[] } if (!Array.isArray(data.data) || data.data.length === 0) { return null } const models = data.data .filter(model => model.model_picker_enabled !== false) .filter(model => model.policy?.state !== 'disabled') .map( (model): CopilotModelInfo => ({ id: `copilot:${model.id}`, label: model.name || model.id, description: `${model.name || model.id} via Copilot`, supportedEndpoints: model.supported_endpoints, }), ) return models.length > 0 ? models : null } export async function fetchCopilotModelsFromModelsDev(): Promise { try { const response = await fetch(MODELS_DEV_URL, { signal: AbortSignal.timeout(10_000), }) if (!response.ok) return FALLBACK_COPILOT_MODELS const data = await response.json() as Record }> const copilotProvider = data['github-copilot'] if (!copilotProvider?.models) return FALLBACK_COPILOT_MODELS const models: CopilotModelInfo[] = Object.entries(copilotProvider.models).map( ([modelId, info]) => ({ id: `copilot:${modelId}`, label: info.name || modelId, description: `${info.name || modelId} via Copilot`, }), ) return models.length > 0 ? models : FALLBACK_COPILOT_MODELS } catch { return FALLBACK_COPILOT_MODELS } } export async function fetchCopilotModels(): Promise { try { const apiModels = await fetchCopilotModelsFromApi() if (apiModels && apiModels.length > 0) { return apiModels } } catch {} return fetchCopilotModelsFromModelsDev() } function hasCopilotEndpointMetadata(models: CopilotModelInfo[]): boolean { return models.some(model => Array.isArray(model.supportedEndpoints)) } function shouldUseCachedCopilotModels(cache: { models: CopilotModelInfo[] fetchedAt: number } | null | undefined): boolean { if (!cache || cache.models.length === 0) return false if (!hasCopilotEndpointMetadata(cache.models)) return false return Date.now() - cache.fetchedAt < 3600_000 } function refreshCopilotModelsCacheInBackground(): void { if (copilotModelsRefreshPromise || !isCopilotConnected()) { return } copilotModelsRefreshPromise = (async () => { const models = await fetchCopilotModels() cachedModels = models saveGlobalConfig(current => ({ ...current, copilotModelsCache: { models, fetchedAt: Date.now() }, })) })() .catch(() => {}) .finally(() => { copilotModelsRefreshPromise = null }) } export async function getCopilotModels(): Promise { if (cachedModels) return filterUnavailableCopilotModels(cachedModels) const config = getGlobalConfig() const cached = config.copilotModelsCache if (shouldUseCachedCopilotModels(cached)) { cachedModels = cached.models return filterUnavailableCopilotModels(cachedModels) } const models = await fetchCopilotModels() cachedModels = models saveGlobalConfig(current => ({ ...current, copilotModelsCache: { models, fetchedAt: Date.now() }, })) return filterUnavailableCopilotModels(models) } export function getCopilotModelsCached(): CopilotModelInfo[] { if (cachedModels) { if (!hasCopilotEndpointMetadata(cachedModels)) { refreshCopilotModelsCacheInBackground() } return filterUnavailableCopilotModels(cachedModels) } const config = getGlobalConfig() const cached = config.copilotModelsCache if (cached && cached.models.length > 0) { cachedModels = cached.models if (!shouldUseCachedCopilotModels(cached)) { refreshCopilotModelsCacheInBackground() } return filterUnavailableCopilotModels(cachedModels) } refreshCopilotModelsCacheInBackground() return filterUnavailableCopilotModels(FALLBACK_COPILOT_MODELS) } export { FALLBACK_COPILOT_MODELS as COPILOT_MODELS } function getCachedCopilotCompatibility( modelId: string, ): CopilotCompatibilityInfo | undefined { const compatibility = getGlobalConfig().copilotCompatibilityCache?.[modelId] if (!compatibility) return undefined if (Date.now() - compatibility.updatedAt > COPILOT_COMPATIBILITY_CACHE_TTL_MS) { return undefined } return compatibility } function filterUnavailableCopilotModels( models: CopilotModelInfo[], ): CopilotModelInfo[] { return models.filter(model => { if (!supportsCopilotChatCompletions(model)) { return false } const compatibility = getCachedCopilotCompatibility( getCopilotModelId(model.id), ) return ( compatibility?.modelSupported !== false && compatibility?.chatCompletionsSupported !== false ) }) } function saveCopilotCompatibility( modelId: string, updates: Partial, ): void { saveGlobalConfig(current => { const existing = current.copilotCompatibilityCache?.[modelId] const next = { ...existing, ...updates, updatedAt: Date.now(), } satisfies CopilotCompatibilityInfo if ( existing?.outputTokenParam === next.outputTokenParam && existing?.modelSupported === next.modelSupported && existing?.chatCompletionsSupported === next.chatCompletionsSupported ) { return current } return { ...current, copilotCompatibilityCache: { ...current.copilotCompatibilityCache, [modelId]: next, }, } }) } function getPreferredCopilotOutputTokenParam( modelId: string, ): CopilotOutputTokenParam { return ( getCachedCopilotCompatibility(modelId)?.outputTokenParam ?? 'max_tokens' ) } function savePreferredCopilotOutputTokenParam( modelId: string, outputTokenParam: CopilotOutputTokenParam, ): void { saveCopilotCompatibility(modelId, { outputTokenParam }) } function buildCopilotChatRequestBody(params: { modelId: string messages: OpenAIMessage[] isStreaming: boolean maxTokens?: number tools?: OpenAITool[] outputTokenParam: CopilotOutputTokenParam }): Record { const requestBody: Record = { model: params.modelId, messages: params.messages, stream: params.isStreaming, } if (params.maxTokens) { requestBody[params.outputTokenParam] = params.maxTokens } if (params.tools && params.tools.length > 0) { requestBody.tools = params.tools requestBody.tool_choice = 'auto' } return requestBody } async function createCopilotErrorInfo(response: Response): Promise<{ message?: string code?: string }> { const text = await response .clone() .text() .catch(() => '') if (!text) return {} try { const parsed = JSON.parse(text) as { error?: { message?: string; code?: string } } return { message: parsed.error?.message, code: parsed.error?.code, } } catch { return {} } } function createCachedCopilotErrorResponse( message: string, code: string, ): Response { return new Response(JSON.stringify({ error: { message, code } }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } function getSuggestedCopilotOutputTokenParam( currentParam: CopilotOutputTokenParam, errorMessage: string | undefined, ): CopilotOutputTokenParam | undefined { if (!errorMessage) return undefined if ( currentParam === 'max_tokens' && errorMessage.includes("Use 'max_completion_tokens' instead") ) { return 'max_completion_tokens' } if ( currentParam === 'max_completion_tokens' && errorMessage.includes("Use 'max_tokens' instead") ) { return 'max_tokens' } return undefined } async function postCopilotChatCompletion(params: { oauthToken: string requestBody: Record signal?: AbortSignal }): Promise { return fetch(`${COPILOT_API_BASE}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${params.oauthToken}`, 'User-Agent': 'claude-code/2.1.88', 'Openai-Intent': 'conversation-edits', 'x-initiator': 'user', }, body: JSON.stringify(params.requestBody), signal: params.signal, }) } function saveCopilotCompatibilityFromError( modelId: string, errorInfo: { code?: string }, ): void { if (errorInfo.code === 'model_not_supported') { saveCopilotCompatibility(modelId, { modelSupported: false, chatCompletionsSupported: false, }) } else if (errorInfo.code === 'unsupported_api_for_model') { saveCopilotCompatibility(modelId, { chatCompletionsSupported: false, }) } } async function sendCopilotChatCompletion(params: { oauthToken: string modelId: string messages: OpenAIMessage[] isStreaming: boolean maxTokens?: number tools?: OpenAITool[] signal?: AbortSignal }): Promise { const compatibility = getCachedCopilotCompatibility(params.modelId) if (compatibility?.modelSupported === false) { return createCachedCopilotErrorResponse( `Copilot model "${params.modelId}" was previously rejected as unsupported.`, 'model_not_supported', ) } if (compatibility?.chatCompletionsSupported === false) { return createCachedCopilotErrorResponse( `Copilot model "${params.modelId}" does not support the /chat/completions endpoint.`, 'unsupported_api_for_model', ) } let outputTokenParam = getPreferredCopilotOutputTokenParam(params.modelId) let requestBody = buildCopilotChatRequestBody({ modelId: params.modelId, messages: params.messages, isStreaming: params.isStreaming, maxTokens: params.maxTokens, tools: params.tools, outputTokenParam, }) let response = await postCopilotChatCompletion({ oauthToken: params.oauthToken, requestBody, signal: params.signal, }) if (response.ok) { saveCopilotCompatibility(params.modelId, { modelSupported: true, chatCompletionsSupported: true, outputTokenParam, }) return response } const errorInfo = await createCopilotErrorInfo(response) if (!params.maxTokens) { saveCopilotCompatibilityFromError(params.modelId, errorInfo) return response } const suggestedParam = getSuggestedCopilotOutputTokenParam( outputTokenParam, errorInfo.message, ) if (!suggestedParam) { saveCopilotCompatibilityFromError(params.modelId, errorInfo) return response } savePreferredCopilotOutputTokenParam(params.modelId, suggestedParam) outputTokenParam = suggestedParam requestBody = buildCopilotChatRequestBody({ modelId: params.modelId, messages: params.messages, isStreaming: params.isStreaming, maxTokens: params.maxTokens, tools: params.tools, outputTokenParam, }) response = await postCopilotChatCompletion({ oauthToken: params.oauthToken, requestBody, signal: params.signal, }) if (response.ok) { saveCopilotCompatibility(params.modelId, { modelSupported: true, chatCompletionsSupported: true, outputTokenParam, }) return response } saveCopilotCompatibilityFromError( params.modelId, await createCopilotErrorInfo(response), ) return response } type OpenAIMessage = { role: 'system' | 'user' | 'assistant' | 'tool' content: string | Array<{ type: string; text?: string; image_url?: { url: string } }> tool_calls?: Array<{ id: string type: 'function' function: { name: string; arguments: string } }> tool_call_id?: string } type OpenAITool = { type: 'function' function: { name: string description: string parameters: Record } } export type AnthropicContentBlock = | { type: 'text'; text: string } | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } } | { type: 'tool_use'; id: string; name: string; input: Record } | { type: 'tool_result'; tool_use_id: string; content: string | Array<{ type: string; text?: string }> } | { type: 'thinking'; thinking: string } | Record export type AnthropicMessage = { role: 'user' | 'assistant' content: string | AnthropicContentBlock[] } export function convertAnthropicMessagesToOpenAI( messages: AnthropicMessage[], systemPrompt?: string, ): OpenAIMessage[] { const result: OpenAIMessage[] = [] if (systemPrompt) { result.push({ role: 'system', content: systemPrompt }) } for (const msg of messages) { if (typeof msg.content === 'string') { result.push({ role: msg.role, content: msg.content }) continue } if (msg.role === 'user') { const parts: Array<{ type: string; text?: string; image_url?: { url: string } }> = [] const toolResults: OpenAIMessage[] = [] for (const block of msg.content) { if (block.type === 'text') { parts.push({ type: 'text', text: (block as { type: 'text'; text: string }).text }) } else if (block.type === 'image') { const imgBlock = block as { type: 'image'; source: { type: 'base64'; media_type: string; data: string } } parts.push({ type: 'image_url', image_url: { url: `data:${imgBlock.source.media_type};base64,${imgBlock.source.data}` }, }) } else if (block.type === 'tool_result') { const trBlock = block as { type: 'tool_result'; tool_use_id: string; content: string | Array<{ type: string; text?: string }> } let content = '' if (typeof trBlock.content === 'string') { content = trBlock.content } else if (Array.isArray(trBlock.content)) { content = trBlock.content .filter(c => c.type === 'text') .map(c => c.text || '') .join('\n') } toolResults.push({ role: 'tool', content, tool_call_id: trBlock.tool_use_id, }) } } if (toolResults.length > 0) { result.push(...toolResults) if (parts.length > 0) { result.push({ role: 'user', content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text! : parts }) } } else if (parts.length > 0) { result.push({ role: 'user', content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text! : parts }) } } else if (msg.role === 'assistant') { const textParts: string[] = [] const toolCalls: Array<{ id: string; type: 'function'; function: { name: string; arguments: string } }> = [] for (const block of msg.content) { if (block.type === 'text') { textParts.push((block as { type: 'text'; text: string }).text) } else if (block.type === 'tool_use') { const tuBlock = block as { type: 'tool_use'; id: string; name: string; input: Record } toolCalls.push({ id: tuBlock.id, type: 'function', function: { name: tuBlock.name, arguments: JSON.stringify(tuBlock.input), }, }) } } const assistantMsg: OpenAIMessage = { role: 'assistant', content: textParts.join('\n') || '', } if (toolCalls.length > 0) { assistantMsg.tool_calls = toolCalls } result.push(assistantMsg) } } return result } export function convertAnthropicToolsToOpenAI( tools: Array<{ name: string; description?: string; input_schema?: Record }>, ): OpenAITool[] { return tools.map(tool => ({ type: 'function' as const, function: { name: tool.name, description: tool.description || '', parameters: tool.input_schema || { type: 'object', properties: {} }, }, })) } export type CopilotStreamEvent = | { type: 'message_start'; message: { id: string; type: 'message'; role: 'assistant'; model: string; content: []; usage: { input_tokens: number; output_tokens: number } } } | { type: 'content_block_start'; index: number; content_block: { type: 'text'; text: string } } | { type: 'content_block_delta'; index: number; delta: { type: 'text_delta'; text: string } } | { type: 'content_block_stop'; index: number } | { type: 'message_delta'; delta: { stop_reason: string }; usage: { output_tokens: number } } | { type: 'message_stop' } export async function* streamCopilotRequest( model: string, messages: AnthropicMessage[], systemPrompt: string | undefined, tools: Array<{ name: string; description?: string; input_schema?: Record }>, signal: AbortSignal, ): AsyncGenerator { const provider = getCopilotProvider() if (!provider?.oauthToken) { throw new Error('GitHub Copilot is not connected. Use /connect to authenticate.') } const copilotModelId = getCopilotModelId(model) const openaiMessages = convertAnthropicMessagesToOpenAI(messages, systemPrompt) const openaiTools = tools.length > 0 ? convertAnthropicToolsToOpenAI(tools) : undefined const response = await sendCopilotChatCompletion({ oauthToken: provider.oauthToken, modelId: copilotModelId, messages: openaiMessages, isStreaming: true, maxTokens: 16384, tools: openaiTools, signal, }) if (!response.ok) { const errorText = await response.text().catch(() => 'unknown error') throw new Error(`Copilot API error (${response.status}): ${errorText}`) } if (!response.body) { throw new Error('No response body from Copilot API') } const messageId = `msg_copilot_${Date.now()}` let contentIndex = 0 let hasStartedContent = false let currentToolCallIndex = -1 const toolCalls: Map = new Map() let totalOutputTokens = 0 yield { type: 'message_start', message: { id: messageId, type: 'message', role: 'assistant', model: copilotModelId, content: [], usage: { input_tokens: 0, output_tokens: 0 }, }, } const reader = response.body.getReader() const decoder = new TextDecoder() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (!line.startsWith('data: ')) continue const data = line.slice(6).trim() if (data === '[DONE]') { if (hasStartedContent) { yield { type: 'content_block_stop', index: contentIndex - 1 } } for (const [idx, tc] of toolCalls) { yield { type: 'content_block_stop', index: contentIndex + idx, } } yield { type: 'message_delta', delta: { stop_reason: toolCalls.size > 0 ? 'tool_use' : 'end_turn' }, usage: { output_tokens: totalOutputTokens }, } yield { type: 'message_stop' } return } let chunk: { choices?: Array<{ delta?: { content?: string | null tool_calls?: Array<{ index: number id?: string function?: { name?: string; arguments?: string } }> role?: string } finish_reason?: string | null }> usage?: { completion_tokens?: number; prompt_tokens?: number; total_tokens?: number } } try { chunk = JSON.parse(data) } catch { continue } if (chunk.usage?.completion_tokens) { totalOutputTokens = chunk.usage.completion_tokens } const choice = chunk.choices?.[0] if (!choice?.delta) continue const delta = choice.delta if (delta.content != null && delta.content !== '') { if (!hasStartedContent) { hasStartedContent = true yield { type: 'content_block_start', index: contentIndex, content_block: { type: 'text', text: '' }, } } yield { type: 'content_block_delta', index: contentIndex, delta: { type: 'text_delta', text: delta.content }, } } if (delta.tool_calls) { for (const tc of delta.tool_calls) { if (tc.id) { if (hasStartedContent && currentToolCallIndex === -1) { yield { type: 'content_block_stop', index: contentIndex } contentIndex++ hasStartedContent = false } currentToolCallIndex = tc.index toolCalls.set(tc.index, { id: tc.id, name: tc.function?.name || '', arguments: tc.function?.arguments || '', }) const toolBlockIndex = hasStartedContent ? contentIndex + 1 + tc.index : contentIndex + tc.index yield { type: 'content_block_start' as const, index: toolBlockIndex, content_block: { type: 'text', text: '', } as any, } } else if (tc.function?.arguments) { const existing = toolCalls.get(tc.index) if (existing) { existing.arguments += tc.function.arguments } } } } if (choice.finish_reason) { if (hasStartedContent) { yield { type: 'content_block_stop', index: contentIndex } } yield { type: 'message_delta', delta: { stop_reason: choice.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn' }, usage: { output_tokens: totalOutputTokens }, } yield { type: 'message_stop' } return } } } } finally { reader.releaseLock() } yield { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: totalOutputTokens }, } yield { type: 'message_stop' } } export function createCopilotFetchOverride( model: string, ): (input: RequestInfo | URL, init?: RequestInit) => Promise { const provider = getCopilotProvider() if (!provider?.oauthToken) { throw new Error('GitHub Copilot is not connected') } const copilotModelId = getCopilotModelId(model) return async (input: RequestInfo | URL, init?: RequestInit): Promise => { const url = input instanceof URL ? input.href : typeof input === 'string' ? input : input.url if (!url.includes('/messages') && !url.includes('/v1/')) { return fetch(input, init) } if (url.includes('/count_tokens') || url.includes('/models')) { return new Response(JSON.stringify({ input_tokens: 0 }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } let anthropicBody: Record = {} if (init?.body) { try { anthropicBody = JSON.parse(typeof init.body === 'string' ? init.body : new TextDecoder().decode(init.body as ArrayBuffer)) } catch { return fetch(input, init) } } const systemBlocks = anthropicBody.system as Array<{ type: string; text: string }> | string | undefined let systemPrompt = '' if (typeof systemBlocks === 'string') { systemPrompt = systemBlocks } else if (Array.isArray(systemBlocks)) { systemPrompt = systemBlocks .filter(b => b.type === 'text') .map(b => b.text) .join('\n\n') } const anthropicMessages = (anthropicBody.messages || []) as AnthropicMessage[] const openaiMessages = convertAnthropicMessagesToOpenAI(anthropicMessages, systemPrompt) const anthropicTools = (anthropicBody.tools || []) as Array<{ name: string; description?: string; input_schema?: Record }> const openaiTools = anthropicTools.length > 0 ? convertAnthropicToolsToOpenAI(anthropicTools) : undefined const isStreaming = anthropicBody.stream === true const maxTokens = typeof anthropicBody.max_tokens === 'number' ? anthropicBody.max_tokens : undefined const copilotResponse = await sendCopilotChatCompletion({ oauthToken: provider.oauthToken, modelId: copilotModelId, messages: openaiMessages, isStreaming, maxTokens, tools: openaiTools, signal: init?.signal, }) if (!copilotResponse.ok) { return copilotResponse } if (!isStreaming) { const data = await copilotResponse.json() as { id: string choices: Array<{ message: { role: string content: string | null tool_calls?: Array<{ id: string function: { name: string; arguments: string } }> } finish_reason: string }> usage?: { prompt_tokens: number; completion_tokens: number } } const choice = data.choices[0] const anthropicContent: Array<{ type: string text?: string id?: string name?: string input?: unknown }> = [] if (choice?.message?.content) { anthropicContent.push({ type: 'text', text: choice.message.content }) } if (choice?.message?.tool_calls) { for (const tc of choice.message.tool_calls) { anthropicContent.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments || '{}'), }) } } const anthropicResponse = { id: data.id || `msg_copilot_${Date.now()}`, type: 'message', role: 'assistant', content: anthropicContent, model: copilotModelId, stop_reason: choice?.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn', usage: { input_tokens: data.usage?.prompt_tokens || 0, output_tokens: data.usage?.completion_tokens || 0, }, } return new Response(JSON.stringify(anthropicResponse), { status: 200, headers: { 'Content-Type': 'application/json' }, }) } const transformStream = convertOpenAIStreamToAnthropic(copilotResponse.body!, copilotModelId) return new Response(transformStream, { status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }) } } export function convertOpenAIStreamToAnthropic( openaiStream: ReadableStream, model: string, ): ReadableStream { const encoder = new TextEncoder() const decoder = new TextDecoder() let messageId = `msg_${Date.now()}` let contentIndex = 0 let hasStartedContent = false let currentToolCallIndex = -1 const toolCalls: Map = new Map() let totalOutputTokens = 0 return new ReadableStream({ async start(controller) { const reader = openaiStream.getReader() let buffer = '' try { while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (!line.startsWith('data: ')) continue const data = line.slice(6).trim() if (data === '[DONE]') { if (hasStartedContent) { controller.enqueue(encoder.encode(`event: content_block_stop\ndata: {"index":${contentIndex - 1}}\n\n`)) } for (const [idx, tc] of toolCalls) { controller.enqueue( encoder.encode(`event: content_block_stop\ndata: {"index":${contentIndex + idx}}\n\n`), ) } controller.enqueue( encoder.encode( `event: message_delta\ndata: {"delta":{"stop_reason":"${toolCalls.size > 0 ? 'tool_use' : 'end_turn'}"},"usage":{"output_tokens":${totalOutputTokens}}}\n\n`, ), ) controller.enqueue(encoder.encode('event: message_stop\ndata: {}\n\n')) return } let chunk: { choices?: Array<{ delta?: { content?: string | null tool_calls?: Array<{ index: number id?: string function?: { name?: string; arguments?: string } }> role?: string } finish_reason?: string | null }> usage?: { completion_tokens?: number; prompt_tokens?: number; total_tokens?: number } } try { chunk = JSON.parse(data) } catch { continue } if (chunk.usage?.completion_tokens) { totalOutputTokens = chunk.usage.completion_tokens } const choice = chunk.choices?.[0] if (!choice?.delta) continue const delta = choice.delta if (delta.content != null && delta.content !== '') { if (!hasStartedContent) { hasStartedContent = true controller.enqueue( encoder.encode( `event: content_block_start\ndata: {"index":${contentIndex},"content_block":{"type":"text","text":""}}\n\n`, ), ) } controller.enqueue( encoder.encode( `event: content_block_delta\ndata: {"index":${contentIndex},"delta":{"type":"text_delta","text":"${JSON.stringify(delta.content).slice(1, -1)}"}}\n\n`, ), ) } if (delta.tool_calls) { for (const tc of delta.tool_calls) { if (tc.id) { if (hasStartedContent && currentToolCallIndex === -1) { controller.enqueue( encoder.encode(`event: content_block_stop\ndata: {"index":${contentIndex}}\n\n`), ) contentIndex++ hasStartedContent = false } currentToolCallIndex = tc.index toolCalls.set(tc.index, { id: tc.id, name: tc.function?.name || '', arguments: tc.function?.arguments || '', }) const toolBlockIndex = hasStartedContent ? contentIndex + 1 + tc.index : contentIndex + tc.index controller.enqueue( encoder.encode( `event: content_block_start\ndata: {"index":${toolBlockIndex},"content_block":{"type":"text","text":""}}\n\n`, ), ) } else if (tc.function?.arguments) { const existing = toolCalls.get(tc.index) if (existing) { existing.arguments += tc.function.arguments } } } } if (choice.finish_reason) { if (hasStartedContent) { controller.enqueue( encoder.encode(`event: content_block_stop\ndata: {"index":${contentIndex}}\n\n`), ) } controller.enqueue( encoder.encode( `event: message_delta\ndata: {"delta":{"stop_reason":"${choice.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn'}"},"usage":{"output_tokens":${totalOutputTokens}}}\n\n`, ), ) controller.enqueue(encoder.encode('event: message_stop\ndata: {}\n\n')) return } } } } finally { reader.releaseLock() controller.close() } }, }) }