1package provider
2
3import (
4 "context"
5 "strings"
6)
7
8type copilotRequestKey struct{}
9type copilotRequestMetadata struct {
10 initiator string
11 vision bool
12}
13
14type copilotClient struct {
15 router *modelRouter
16}
17
18func (c *copilotClient) Name() string { return "github-copilot" }
19
20func (c *copilotClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
21 metadata := copilotRequestMetadata{initiator: "user"}
22 if len(req.Messages) > 0 && req.Messages[len(req.Messages)-1].Role != RoleUser {
23 metadata.initiator = "agent"
24 }
25 for _, message := range req.Messages {
26 if message.Role == RoleUser || message.Role == RoleTool {
27 metadata.vision = metadata.vision || copilotHasImages(message.Content)
28 }
29 }
30 // These endpoints perform their own thinking; they reject reasoning_effort.
31 api := copilotModelAPI(req.Model)
32 if model, err := FindModel(c.Name(), req.Model); err == nil && model.API != "" {
33 api = model.API
34 }
35 if api == APICompletions {
36 req.Reasoning = ""
37 }
38 ctx = context.WithValue(ctx, copilotRequestKey{}, metadata)
39 // Use family routing for custom models that have not supplied an API tag.
40 if model, err := FindModel(c.Name(), req.Model); err != nil || model.API == "" {
41 return c.router.byAPI[api].Stream(ctx, req)
42 }
43 return c.router.Stream(ctx, req)
44}
45
46func copilotHasImages(blocks []Content) bool {
47 for _, block := range blocks {
48 switch b := block.(type) {
49 case ImageBlock:
50 return true
51 case ToolResultBlock:
52 if copilotHasImages(b.Content) {
53 return true
54 }
55 }
56 }
57 return false
58}
59
60func copilotModelAPI(id string) string {
61 switch {
62 case strings.HasPrefix(id, "claude-"):
63 return APIAnthropicMessages
64 case strings.HasPrefix(id, "gpt-"), strings.HasPrefix(id, "grok-"), strings.HasPrefix(id, "mai-"), strings.HasPrefix(id, "oswe"):
65 return APIResponses
66 default:
67 return APICompletions
68 }
69}
70
71func configureCopilotModel(m *Model) {
72 m.API = copilotModelAPI(m.ID)
73 if m.API == APIAnthropicMessages && !strings.HasPrefix(m.ID, "claude-haiku-") {
74 m.AdaptiveThinking = true
75 }
76 if m.API == APICompletions {
77 m.ReasoningLevelMap = map[string]string{"minimum": "", "low": "", "medium": "", "high": "", "xhigh": "", "max": ""}
78 }
79 // Responses defaults expose xhigh, but these models only support low/medium/high.
80 if m.ID == "gpt-5-mini" || m.ID == "grok-4.5" || strings.HasPrefix(m.ID, "mai-") {
81 m.ReasoningLevelMap = map[string]string{"xhigh": "", "max": ""}
82 }
83}