v0.51.0 a9d674e

github_copilot_request.go RawHistoryFolder

1package provider
2
3import (
4	"context"
5	"fmt"
6	"regexp"
7	"strings"
8)
9
10type copilotRequestKey struct{}
11type copilotRequestMetadata struct {
12	initiator string
13	vision    bool
14}
15
16type copilotClient struct {
17	router *modelRouter
18}
19
20func (c *copilotClient) Name() string { return "github-copilot" }
21
22// SetModelMetadata lets runtime-owned model switches reach the wire clients.
23func (c *copilotClient) SetModelMetadata(model Model) { c.router.SetModelMetadata(model) }
24
25func (c *copilotClient) Stream(ctx context.Context, req Request) (<-chan Event, error) {
26	metadata := copilotRequestMetadata{initiator: "user"}
27	if len(req.Messages) > 0 && req.Messages[len(req.Messages)-1].Role != RoleUser {
28		metadata.initiator = "agent"
29	}
30	for _, message := range req.Messages {
31		if message.Role == RoleUser || message.Role == RoleTool {
32			metadata.vision = metadata.vision || copilotHasImages(message.Content)
33		}
34	}
35	// One catalog lookup decides both reasoning and route.
36	model, findErr := FindModel(c.Name(), req.Model)
37	tagged := findErr == nil && model.API != ""
38	api := copilotModelAPI(req.Model)
39	if tagged {
40		api = model.API
41	}
42	// Chat Completions endpoints perform their own thinking; they reject
43	// reasoning_effort.
44	if api == APICompletions {
45		req.Reasoning = ""
46	}
47	ctx = context.WithValue(ctx, copilotRequestKey{}, metadata)
48	if tagged {
49		return c.router.Stream(ctx, req)
50	}
51	// Use family routing for custom models that have not supplied an API tag.
52	routed := c.router.byAPI[api]
53	if routed == nil {
54		return nil, fmt.Errorf("provider %q model %q has no client for wire API %q", c.Name(), req.Model, api)
55	}
56	return routed.Stream(ctx, req)
57}
58
59func copilotHasImages(blocks []Content) bool {
60	for _, block := range blocks {
61		switch b := block.(type) {
62		case ImageBlock:
63			return true
64		case ToolResultBlock:
65			if copilotHasImages(b.Content) {
66				return true
67			}
68		}
69	}
70	return false
71}
72
73func copilotModelAPI(id string) string {
74	switch {
75	case strings.HasPrefix(id, "claude-"):
76		return APIAnthropicMessages
77	case strings.HasPrefix(id, "gpt-4"), strings.HasPrefix(id, "grok-code-"):
78		// Legacy chat-only families keep Chat Completions.
79		return APICompletions
80	case strings.HasPrefix(id, "gpt-"), strings.HasPrefix(id, "grok-"), strings.HasPrefix(id, "mai-"), strings.HasPrefix(id, "oswe"):
81		return APIResponses
82	default:
83		return APICompletions
84	}
85}
86
87func configureCopilotModel(m *Model) {
88	switch m.API {
89	case "":
90		m.API = copilotModelAPI(m.ID)
91	case "anthropic":
92		// Legacy models.json api values map onto Copilot's wire protocols.
93		m.API = APIAnthropicMessages
94	case "openai":
95		m.API = APICompletions
96	}
97	// Claude Haiku, 4.5 and 4.6 use explicit budgets: the canonical Anthropic
98	// metadata (usesAdaptiveThinking) does not treat 4.6 as adaptive. Newer or
99	// unknown Claude ids keep the adaptive default; explicit metadata wins.
100	if m.API == APIAnthropicMessages && !usesExplicitThinkingBudget(m.ID) {
101		m.AdaptiveThinking = true
102	}
103	// An explicit mapping (for example from models.json) always wins.
104	if m.ReasoningLevelMap != nil {
105		return
106	}
107	if m.API == APICompletions {
108		m.ReasoningLevelMap = map[string]string{"minimum": "", "low": "", "medium": "", "high": "", "xhigh": "", "max": ""}
109	}
110	// Responses defaults expose xhigh, but these models only support low/medium/high.
111	if m.ID == "gpt-5-mini" || m.ID == "grok-4.5" || strings.HasPrefix(m.ID, "mai-") {
112		m.ReasoningLevelMap = map[string]string{"xhigh": "", "max": ""}
113	}
114}
115
116func usesExplicitThinkingBudget(id string) bool {
117	if strings.HasPrefix(id, "claude-haiku-") {
118		return true
119	}
120	// Claude 4 through 4.6 predate adaptive thinking (4.7 is the first).
121	return copilotLegacyClaudeVersion.MatchString(id)
122}
123
124var copilotLegacyClaudeVersion = regexp.MustCompile(`-4(\.[0-6])?$`)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close