Merge pull request #171 from ksp2000/feature/copilot-auto-model-buckets

refactor(copilot): use auto model buckets for transcript inference
This commit is contained in:
Resham Joshi 2026-04-28 12:17:50 -07:00 committed by GitHub
commit fbb2c4e69c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 129 additions and 3 deletions

View file

@ -133,6 +133,8 @@ const BUILTIN_ALIASES: Record<string, string> = {
'cursor-auto': 'claude-sonnet-4-5',
'cursor-agent-auto': 'claude-sonnet-4-5',
'copilot-auto': 'claude-sonnet-4-5',
'copilot-openai-auto': 'gpt-5.3-codex',
'copilot-anthropic-auto': 'claude-sonnet-4-5',
'kiro-auto': 'claude-sonnet-4-5',
'cline-auto': 'claude-sonnet-4-5',
'openclaw-auto': 'claude-sonnet-4-5',

View file

@ -13,8 +13,16 @@ const modelDisplayNames: Record<string, string> = {
'gpt-4.1': 'GPT-4.1',
'gpt-4o-mini': 'GPT-4o Mini',
'gpt-4o': 'GPT-4o',
'gpt-5.4': 'GPT-5.4',
'gpt-5.3-codex': 'GPT-5.3 Codex',
'gpt-5-mini': 'GPT-5 Mini',
'gpt-5': 'GPT-5',
'claude-opus-4-7': 'Opus 4.7',
'claude-opus-4-6': 'Opus 4.6',
'claude-opus-4-5': 'Opus 4.5',
'claude-opus-4-1': 'Opus 4.1',
'claude-opus-4': 'Opus 4',
'claude-sonnet-4-6': 'Sonnet 4.6',
'claude-sonnet-4-5': 'Sonnet 4.5',
'claude-sonnet-4': 'Sonnet 4',
'claude-3-7-sonnet': 'Sonnet 3.7',
@ -45,6 +53,8 @@ const toolNameMap: Record<string, string> = {
}
const CHARS_PER_TOKEN = 4
const COPILOT_OPENAI_AUTO = 'copilot-openai-auto'
const COPILOT_ANTHROPIC_AUTO = 'copilot-anthropic-auto'
const modelDisplayEntries = Object.entries(modelDisplayNames).sort((a, b) => b[0].length - a[0].length)
@ -143,15 +153,35 @@ type TranscriptEvent =
| { type: 'assistant.message'; timestamp?: string; data: { messageId: string; content?: string; reasoningText?: string; toolRequests?: TranscriptToolRequest[]; outputTokens?: number } }
| { type: string; timestamp?: string; data: Record<string, unknown> }
const transcriptToolCallModelHints: Array<{ prefix: string; model: string }> = [
// Anthropic tool-call ID variants observed in Copilot transcript logs.
{ prefix: 'toolu_bdrk_', model: COPILOT_ANTHROPIC_AUTO },
{ prefix: 'toolu_vrtx_', model: COPILOT_ANTHROPIC_AUTO },
{ prefix: 'tooluse_', model: COPILOT_ANTHROPIC_AUTO },
// OpenAI tool-call IDs.
{ prefix: 'call_', model: COPILOT_OPENAI_AUTO },
]
function inferModelFromToolCallIds(events: TranscriptEvent[]): string {
const modelCounts = new Map<string, number>()
for (const e of events) {
if (e.type !== 'assistant.message') continue
const msg = e as { data: { toolRequests?: TranscriptToolRequest[] } }
for (const t of msg.data.toolRequests ?? []) {
if (t.toolCallId?.startsWith('toolu_bdrk_')) return 'claude-sonnet-4-5'
if (t.toolCallId?.startsWith('call_')) return 'gpt-4.1'
const toolCallId = t.toolCallId ?? ''
for (const hint of transcriptToolCallModelHints) {
if (!toolCallId.startsWith(hint.prefix)) continue
modelCounts.set(hint.model, (modelCounts.get(hint.model) ?? 0) + 1)
break
}
}
}
if (modelCounts.size > 0) {
return [...modelCounts.entries()].sort((a, b) => b[1] - a[1])[0]![0]
}
return 'copilot-auto'
}
@ -375,6 +405,8 @@ export function createCopilotProvider(sessionStateDir?: string, workspaceStorage
modelDisplayName(model: string): string {
if (model === 'copilot-auto') return 'Copilot (auto)'
if (model === COPILOT_OPENAI_AUTO) return 'Copilot (OpenAI auto)'
if (model === COPILOT_ANTHROPIC_AUTO) return 'Copilot (Anthropic auto)'
for (const [key, name] of modelDisplayEntries) {
if (model === key || model.startsWith(key + '-')) return name
}

View file

@ -37,6 +37,30 @@ function assistantMessage(opts: { messageId: string; outputTokens: number; tools
})
}
function transcriptSessionStart(sessionId: string) {
return JSON.stringify({ type: 'session.start', data: { sessionId, producer: 'copilot-agent' } })
}
function transcriptUserMessage(content: string) {
return JSON.stringify({ type: 'user.message', data: { content, attachments: [] } })
}
function transcriptAssistantMessage(opts: { messageId: string; content?: string; reasoningText?: string; toolCallIds?: string[] }) {
return JSON.stringify({
type: 'assistant.message',
data: {
messageId: opts.messageId,
content: opts.content ?? '',
reasoningText: opts.reasoningText ?? '',
toolRequests: (opts.toolCallIds ?? []).map((id, i) => ({
toolCallId: id,
name: i === 0 ? 'read_file' : 'run_in_terminal',
type: 'function',
})),
},
})
}
describe('copilot provider - JSONL parsing', () => {
beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'copilot-test-'))
@ -155,10 +179,76 @@ describe('copilot provider - JSONL parsing', () => {
for await (const call of copilot.createSessionParser(source, new Set()).parse()) calls.push(call)
expect(calls).toHaveLength(1)
expect(calls[0]!.messageId).toBeUndefined()
expect(calls[0]!.outputTokens).toBe(80)
expect(calls[0]!.model).toBe('gpt-4.1')
})
it('infers OpenAI auto bucket for transcript toolCallId prefix call_', async () => {
const eventsPath = await createSessionDir('sess-tr-call', [
transcriptSessionStart('sess-tr-call'),
transcriptUserMessage('check model inference'),
transcriptAssistantMessage({
messageId: 'msg-1',
content: 'done',
toolCallIds: ['call_abc123'],
}),
])
const source = { path: eventsPath, project: 'test', provider: 'copilot' }
const calls: ParsedProviderCall[] = []
for await (const call of copilot.createSessionParser(source, new Set()).parse()) calls.push(call)
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('copilot-openai-auto')
})
it('infers Anthropic auto bucket for transcript toolCallId prefixes tooluse_/toolu_vrtx_', async () => {
const eventsPath = await createSessionDir('sess-tr-claude', [
transcriptSessionStart('sess-tr-claude'),
transcriptUserMessage('check model inference'),
transcriptAssistantMessage({
messageId: 'msg-1',
content: 'done',
toolCallIds: ['tooluse_XY', 'toolu_vrtx_01ABC'],
}),
])
const source = { path: eventsPath, project: 'test', provider: 'copilot' }
const calls: ParsedProviderCall[] = []
for await (const call of copilot.createSessionParser(source, new Set()).parse()) calls.push(call)
expect(calls).toHaveLength(1)
expect(calls[0]!.model).toBe('copilot-anthropic-auto')
})
it('chooses the dominant inferred transcript model when prefixes are mixed', async () => {
const eventsPath = await createSessionDir('sess-tr-mixed', [
transcriptSessionStart('sess-tr-mixed'),
transcriptUserMessage('mixed'),
transcriptAssistantMessage({
messageId: 'msg-1',
content: 'one',
toolCallIds: ['toolu_bdrk_123'],
}),
transcriptAssistantMessage({
messageId: 'msg-2',
content: 'two',
toolCallIds: ['call_1'],
}),
transcriptAssistantMessage({
messageId: 'msg-3',
content: 'three',
toolCallIds: ['call_2'],
}),
])
const source = { path: eventsPath, project: 'test', provider: 'copilot' }
const calls: ParsedProviderCall[] = []
for await (const call of copilot.createSessionParser(source, new Set()).parse()) calls.push(call)
expect(calls).toHaveLength(3)
expect(calls.every(c => c.model === 'copilot-openai-auto')).toBe(true)
})
})
describe('copilot provider - discoverSessions', () => {
@ -257,6 +347,8 @@ describe('copilot provider - metadata', () => {
expect(copilot.modelDisplayName('gpt-5-mini')).toBe('GPT-5 Mini')
expect(copilot.modelDisplayName('o3')).toBe('o3')
expect(copilot.modelDisplayName('o4-mini')).toBe('o4-mini')
expect(copilot.modelDisplayName('copilot-openai-auto')).toBe('Copilot (OpenAI auto)')
expect(copilot.modelDisplayName('copilot-anthropic-auto')).toBe('Copilot (Anthropic auto)')
expect(copilot.modelDisplayName('unknown-model-xyz')).toBe('unknown-model-xyz')
})