History

feat(registry): add supported endpoints for GitHub Copilot models

Enhance model definitions by including supported API endpoints for each model. This allows for better integration and usage tracking with the GitHub Copilot API.

ChrAlpha committed 10 months ago

Commit 0ffcce31 parent f4fcfc54 files · +132 −9Browse files

Unified · Split

4 files
  1. internal/registry/model_definitions.go +43 −6
  2. internal/registry/model_registry.go +8 −0
  3. internal/runtime/executor/github_copilot_executor.go +33 −3
  4. internal/runtime/executor/usage_helpers.go +48 −0
internal/registry/model_definitions.go+43−6
@@ -834,6 +834,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 833 unmodified lines
834 Description: "OpenAI GPT-5 via GitHub Copilot",834 Description: "OpenAI GPT-5 via GitHub Copilot",
835 ContextLength: 200000,835 ContextLength: 200000,
836 MaxCompletionTokens: 32768,836 MaxCompletionTokens: 32768,
837 SupportedEndpoints: []string{"/chat/completions", "/responses"},
837 },838 },
838 {839 {
839 ID: "gpt-5-mini",840 ID: "gpt-5-mini",
@@ -845,6 +846,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
845 Description: "OpenAI GPT-5 Mini via GitHub Copilot",846 Description: "OpenAI GPT-5 Mini via GitHub Copilot",
846 ContextLength: 128000,847 ContextLength: 128000,
847 MaxCompletionTokens: 16384,848 MaxCompletionTokens: 16384,
849 SupportedEndpoints: []string{"/chat/completions", "/responses"},
848 },850 },
849 {851 {
850 ID: "gpt-5-codex",852 ID: "gpt-5-codex",
@@ -856,6 +858,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
856 Description: "OpenAI GPT-5 Codex via GitHub Copilot",858 Description: "OpenAI GPT-5 Codex via GitHub Copilot",
857 ContextLength: 200000,859 ContextLength: 200000,
858 MaxCompletionTokens: 32768,860 MaxCompletionTokens: 32768,
861 SupportedEndpoints: []string{"/responses"},
859 },862 },
860 {863 {
861 ID: "gpt-5.1",864 ID: "gpt-5.1",
@@ -867,6 +870,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
867 Description: "OpenAI GPT-5.1 via GitHub Copilot",870 Description: "OpenAI GPT-5.1 via GitHub Copilot",
868 ContextLength: 200000,871 ContextLength: 200000,
869 MaxCompletionTokens: 32768,872 MaxCompletionTokens: 32768,
873 SupportedEndpoints: []string{"/chat/completions", "/responses"},
870 },874 },
871 {875 {
872 ID: "gpt-5.1-codex",876 ID: "gpt-5.1-codex",
@@ -878,6 +882,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
878 Description: "OpenAI GPT-5.1 Codex via GitHub Copilot",882 Description: "OpenAI GPT-5.1 Codex via GitHub Copilot",
879 ContextLength: 200000,883 ContextLength: 200000,
880 MaxCompletionTokens: 32768,884 MaxCompletionTokens: 32768,
885 SupportedEndpoints: []string{"/responses"},
881 },886 },
882 {887 {
883 ID: "gpt-5.1-codex-mini",888 ID: "gpt-5.1-codex-mini",
@@ -889,6 +894,19 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
889 Description: "OpenAI GPT-5.1 Codex Mini via GitHub Copilot",894 Description: "OpenAI GPT-5.1 Codex Mini via GitHub Copilot",
890 ContextLength: 128000,895 ContextLength: 128000,
891 MaxCompletionTokens: 16384,896 MaxCompletionTokens: 16384,
897 SupportedEndpoints: []string{"/responses"},
898 },
899 {
900 ID: "gpt-5.1-codex-max",
901 Object: "model",
902 Created: now,
903 OwnedBy: "github-copilot",
904 Type: "github-copilot",
905 DisplayName: "GPT-5.1 Codex Max",
906 Description: "OpenAI GPT-5.1 Codex Max via GitHub Copilot",
907 ContextLength: 200000,
908 MaxCompletionTokens: 32768,
909 SupportedEndpoints: []string{"/responses"},
892 },910 },
893 {911 {
894 ID: "gpt-5.2",912 ID: "gpt-5.2",
@@ -900,6 +918,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
900 Description: "OpenAI GPT-5.2 via GitHub Copilot",918 Description: "OpenAI GPT-5.2 via GitHub Copilot",
901 ContextLength: 200000,919 ContextLength: 200000,
902 MaxCompletionTokens: 32768,920 MaxCompletionTokens: 32768,
921 SupportedEndpoints: []string{"/chat/completions", "/responses"},
903 },922 },
904 {923 {
905 ID: "gpt-5.2-codex",924 ID: "gpt-5.2-codex",
@@ -911,6 +930,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
911 Description: "OpenAI GPT-5.2 Codex via GitHub Copilot",930 Description: "OpenAI GPT-5.2 Codex via GitHub Copilot",
912 ContextLength: 200000,931 ContextLength: 200000,
913 MaxCompletionTokens: 32768,932 MaxCompletionTokens: 32768,
933 SupportedEndpoints: []string{"/responses"},
914 },934 },
915 {935 {
916 ID: "claude-haiku-4.5",936 ID: "claude-haiku-4.5",
@@ -922,6 +942,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
922 Description: "Anthropic Claude Haiku 4.5 via GitHub Copilot",942 Description: "Anthropic Claude Haiku 4.5 via GitHub Copilot",
923 ContextLength: 200000,943 ContextLength: 200000,
924 MaxCompletionTokens: 64000,944 MaxCompletionTokens: 64000,
945 SupportedEndpoints: []string{"/chat/completions"},
925 },946 },
926 {947 {
927 ID: "claude-opus-4.1",948 ID: "claude-opus-4.1",
@@ -933,6 +954,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
933 Description: "Anthropic Claude Opus 4.1 via GitHub Copilot",954 Description: "Anthropic Claude Opus 4.1 via GitHub Copilot",
934 ContextLength: 200000,955 ContextLength: 200000,
935 MaxCompletionTokens: 32000,956 MaxCompletionTokens: 32000,
957 SupportedEndpoints: []string{"/chat/completions"},
936 },958 },
937 {959 {
938 ID: "claude-opus-4.5",960 ID: "claude-opus-4.5",
@@ -944,6 +966,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
944 Description: "Anthropic Claude Opus 4.5 via GitHub Copilot",966 Description: "Anthropic Claude Opus 4.5 via GitHub Copilot",
945 ContextLength: 200000,967 ContextLength: 200000,
946 MaxCompletionTokens: 64000,968 MaxCompletionTokens: 64000,
969 SupportedEndpoints: []string{"/chat/completions"},
947 },970 },
948 {971 {
949 ID: "claude-sonnet-4",972 ID: "claude-sonnet-4",
@@ -955,6 +978,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
955 Description: "Anthropic Claude Sonnet 4 via GitHub Copilot",978 Description: "Anthropic Claude Sonnet 4 via GitHub Copilot",
956 ContextLength: 200000,979 ContextLength: 200000,
957 MaxCompletionTokens: 64000,980 MaxCompletionTokens: 64000,
981 SupportedEndpoints: []string{"/chat/completions"},
958 },982 },
959 {983 {
960 ID: "claude-sonnet-4.5",984 ID: "claude-sonnet-4.5",
@@ -966,6 +990,7 @@ func GetGitHubCopilotModels() []*ModelInfo { · 5 unmodified lines
966 Description: "Anthropic Claude Sonnet 4.5 via GitHub Copilot",990 Description: "Anthropic Claude Sonnet 4.5 via GitHub Copilot",
967 ContextLength: 200000,991 ContextLength: 200000,
968 MaxCompletionTokens: 64000,992 MaxCompletionTokens: 64000,
993 SupportedEndpoints: []string{"/chat/completions"},
969 },994 },
970 {995 {
971 ID: "gemini-2.5-pro",996 ID: "gemini-2.5-pro",
@@ -979,13 +1004,24 @@ func GetGitHubCopilotModels() []*ModelInfo { · 7 unmodified lines
979 MaxCompletionTokens: 65536,1004 MaxCompletionTokens: 65536,
980 },1005 },
981 {1006 {
982 ID: "gemini-3-pro",1007 ID: "gemini-3-pro-preview",
1008 Object: "model",
1009 Created: now,
1010 OwnedBy: "github-copilot",
1011 Type: "github-copilot",
1012 DisplayName: "Gemini 3 Pro (Preview)",
1013 Description: "Google Gemini 3 Pro Preview via GitHub Copilot",
1014 ContextLength: 1048576,
1015 MaxCompletionTokens: 65536,
1016 },
1017 {
1018 ID: "gemini-3-flash-preview",
983 Object: "model",1019 Object: "model",
984 Created: now,1020 Created: now,
985 OwnedBy: "github-copilot",1021 OwnedBy: "github-copilot",
986 Type: "github-copilot",1022 Type: "github-copilot",
987 DisplayName: "Gemini 3 Pro",1023 DisplayName: "Gemini 3 Flash (Preview)",
988 Description: "Google Gemini 3 Pro via GitHub Copilot",1024 Description: "Google Gemini 3 Flash Preview via GitHub Copilot",
989 ContextLength: 1048576,1025 ContextLength: 1048576,
990 MaxCompletionTokens: 65536,1026 MaxCompletionTokens: 65536,
991 },1027 },
@@ -1001,15 +1037,16 @@ func GetGitHubCopilotModels() []*ModelInfo { · 9 unmodified lines
1001 MaxCompletionTokens: 16384,1037 MaxCompletionTokens: 16384,
1002 },1038 },
1003 {1039 {
1004 ID: "raptor-mini",1040 ID: "oswe-vscode-prime",
1005 Object: "model",1041 Object: "model",
1006 Created: now,1042 Created: now,
1007 OwnedBy: "github-copilot",1043 OwnedBy: "github-copilot",
1008 Type: "github-copilot",1044 Type: "github-copilot",
1009 DisplayName: "Raptor Mini",1045 DisplayName: "Raptor mini (Preview)",
1010 Description: "Raptor Mini via GitHub Copilot",1046 Description: "Raptor mini via GitHub Copilot",
1011 ContextLength: 128000,1047 ContextLength: 128000,
1012 MaxCompletionTokens: 16384,1048 MaxCompletionTokens: 16384,
1049 SupportedEndpoints: []string{"/chat/completions", "/responses"},
1013 },1050 },
1014 }1051 }
1015}1052}
internal/registry/model_registry.go+8−0
@@ -47,6 +47,8 @@ type ModelInfo struct { · 46 unmodified lines
47 MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`47 MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
48 // SupportedParameters lists supported parameters48 // SupportedParameters lists supported parameters
49 SupportedParameters []string `json:"supported_parameters,omitempty"`49 SupportedParameters []string `json:"supported_parameters,omitempty"`
50 // SupportedEndpoints lists supported API endpoints (e.g., "/chat/completions", "/responses").
51 SupportedEndpoints []string `json:"supported_endpoints,omitempty"`
50 52
51 // Thinking holds provider-specific reasoning/thinking budget capabilities.53 // Thinking holds provider-specific reasoning/thinking budget capabilities.
52 // This is optional and currently used for Gemini thinking budget normalization.54 // This is optional and currently used for Gemini thinking budget normalization.
@@ -456,6 +458,9 @@ func cloneModelInfo(model *ModelInfo) *ModelInfo { · 403 unmodified lines
456 if len(model.SupportedParameters) > 0 {458 if len(model.SupportedParameters) > 0 {
457 copyModel.SupportedParameters = append([]string(nil), model.SupportedParameters...)459 copyModel.SupportedParameters = append([]string(nil), model.SupportedParameters...)
458 }460 }
461 if len(model.SupportedEndpoints) > 0 {
462 copyModel.SupportedEndpoints = append([]string(nil), model.SupportedEndpoints...)
463 }
459 return &copyModel464 return &copyModel
460}465}
461 466
@@ -968,6 +973,9 @@ func (r *ModelRegistry) convertModelToMap(model *ModelInfo, handlerType string) · 506 unmodified lines
968 if len(model.SupportedParameters) > 0 {973 if len(model.SupportedParameters) > 0 {
969 result["supported_parameters"] = model.SupportedParameters974 result["supported_parameters"] = model.SupportedParameters
970 }975 }
976 if len(model.SupportedEndpoints) > 0 {
977 result["supported_endpoints"] = model.SupportedEndpoints
978 }
971 return result979 return result
972 980
973 case "claude", "kiro", "antigravity":981 case "claude", "kiro", "antigravity":
internal/runtime/executor/github_copilot_executor.go+33−3
@@ -23,6 +23,7 @@ import ( · 22 unmodified lines
23const (23const (
24 githubCopilotBaseURL = "https://api.githubcopilot.com"24 githubCopilotBaseURL = "https://api.githubcopilot.com"
25 githubCopilotChatPath = "/chat/completions"25 githubCopilotChatPath = "/chat/completions"
26 githubCopilotResponsesPath = "/responses"
26 githubCopilotAuthType = "github-copilot"27 githubCopilotAuthType = "github-copilot"
27 githubCopilotTokenCacheTTL = 25 * time.Minute28 githubCopilotTokenCacheTTL = 25 * time.Minute
28 // tokenExpiryBuffer is the time before expiry when we should refresh the token.29 // tokenExpiryBuffer is the time before expiry when we should refresh the token.
@@ -106,7 +107,11 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. · 77 unmodified lines
106 defer reporter.trackFailure(ctx, &err)107 defer reporter.trackFailure(ctx, &err)
107 108
108 from := opts.SourceFormat109 from := opts.SourceFormat
110 useResponses := useGitHubCopilotResponsesEndpoint(from)
109 to := sdktranslator.FromString("openai")111 to := sdktranslator.FromString("openai")
112 if useResponses {
113 to = sdktranslator.FromString("openai-response")
114 }
110 originalPayload := bytes.Clone(req.Payload)115 originalPayload := bytes.Clone(req.Payload)
111 if len(opts.OriginalRequest) > 0 {116 if len(opts.OriginalRequest) > 0 {
112 originalPayload = bytes.Clone(opts.OriginalRequest)117 originalPayload = bytes.Clone(opts.OriginalRequest)
@@ -117,7 +122,11 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. · 4 unmodified lines
117 body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated)122 body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated)
118 body, _ = sjson.SetBytes(body, "stream", false)123 body, _ = sjson.SetBytes(body, "stream", false)
119 124
120 url := githubCopilotBaseURL + githubCopilotChatPath125 path := githubCopilotChatPath
126 if useResponses {
127 path = githubCopilotResponsesPath
128 }
129 url := githubCopilotBaseURL + path
121 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))130 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
122 if err != nil {131 if err != nil {
123 return resp, err132 return resp, err
@@ -172,6 +181,9 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth. · 48 unmodified lines
172 appendAPIResponseChunk(ctx, e.cfg, data)181 appendAPIResponseChunk(ctx, e.cfg, data)
173 182
174 detail := parseOpenAIUsage(data)183 detail := parseOpenAIUsage(data)
184 if useResponses && detail.TotalTokens == 0 {
185 detail = parseOpenAIResponsesUsage(data)
186 }
175 if detail.TotalTokens > 0 {187 if detail.TotalTokens > 0 {
176 reporter.publish(ctx, detail)188 reporter.publish(ctx, detail)
177 }189 }
@@ -194,7 +206,11 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox · 16 unmodified lines
194 defer reporter.trackFailure(ctx, &err)206 defer reporter.trackFailure(ctx, &err)
195 207
196 from := opts.SourceFormat208 from := opts.SourceFormat
209 useResponses := useGitHubCopilotResponsesEndpoint(from)
197 to := sdktranslator.FromString("openai")210 to := sdktranslator.FromString("openai")
211 if useResponses {
212 to = sdktranslator.FromString("openai-response")
213 }
198 originalPayload := bytes.Clone(req.Payload)214 originalPayload := bytes.Clone(req.Payload)
199 if len(opts.OriginalRequest) > 0 {215 if len(opts.OriginalRequest) > 0 {
200 originalPayload = bytes.Clone(opts.OriginalRequest)216 originalPayload = bytes.Clone(opts.OriginalRequest)
@@ -205,9 +221,15 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox · 4 unmodified lines
205 body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated)221 body = applyPayloadConfigWithRoot(e.cfg, req.Model, to.String(), "", body, originalTranslated)
206 body, _ = sjson.SetBytes(body, "stream", true)222 body, _ = sjson.SetBytes(body, "stream", true)
207 // Enable stream options for usage stats in stream223 // Enable stream options for usage stats in stream
208 body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)224 if !useResponses {
225 body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)
226 }
209 227
210 url := githubCopilotBaseURL + githubCopilotChatPath228 path := githubCopilotChatPath
229 if useResponses {
230 path = githubCopilotResponsesPath
231 }
232 url := githubCopilotBaseURL + path
211 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))233 httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
212 if err != nil {234 if err != nil {
213 return nil, err235 return nil, err
@@ -283,6 +305,10 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox · 69 unmodified lines
283 }305 }
284 if detail, ok := parseOpenAIStreamUsage(line); ok {306 if detail, ok := parseOpenAIStreamUsage(line); ok {
285 reporter.publish(ctx, detail)307 reporter.publish(ctx, detail)
308 } else if useResponses {
309 if detail, ok := parseOpenAIResponsesStreamUsage(line); ok {
310 reporter.publish(ctx, detail)
311 }
286 }312 }
287 }313 }
288 314
@@ -393,6 +419,10 @@ func (e *GitHubCopilotExecutor) normalizeModel(_ string, body []byte) []byte { · 104 unmodified lines
393 return body419 return body
394}420}
395 421
422func useGitHubCopilotResponsesEndpoint(sourceFormat sdktranslator.Format) bool {
423 return sourceFormat.String() == "openai-response"
424}
425
396// isHTTPSuccess checks if the status code indicates success (2xx).426// isHTTPSuccess checks if the status code indicates success (2xx).
397func isHTTPSuccess(statusCode int) bool {427func isHTTPSuccess(statusCode int) bool {
398 return statusCode >= 200 && statusCode < 300428 return statusCode >= 200 && statusCode < 300
internal/runtime/executor/usage_helpers.go+48−0
@@ -236,6 +236,54 @@ func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) { · 235 unmodified lines
236 return detail, true236 return detail, true
237}237}
238 238
239func parseOpenAIResponsesUsage(data []byte) usage.Detail {
240 usageNode := gjson.ParseBytes(data).Get("usage")
241 if !usageNode.Exists() {
242 return usage.Detail{}
243 }
244 detail := usage.Detail{
245 InputTokens: usageNode.Get("input_tokens").Int(),
246 OutputTokens: usageNode.Get("output_tokens").Int(),
247 TotalTokens: usageNode.Get("total_tokens").Int(),
248 }
249 if detail.TotalTokens == 0 {
250 detail.TotalTokens = detail.InputTokens + detail.OutputTokens
251 }
252 if cached := usageNode.Get("input_tokens_details.cached_tokens"); cached.Exists() {
253 detail.CachedTokens = cached.Int()
254 }
255 if reasoning := usageNode.Get("output_tokens_details.reasoning_tokens"); reasoning.Exists() {
256 detail.ReasoningTokens = reasoning.Int()
257 }
258 return detail
259}
260
261func parseOpenAIResponsesStreamUsage(line []byte) (usage.Detail, bool) {
262 payload := jsonPayload(line)
263 if len(payload) == 0 || !gjson.ValidBytes(payload) {
264 return usage.Detail{}, false
265 }
266 usageNode := gjson.GetBytes(payload, "usage")
267 if !usageNode.Exists() {
268 return usage.Detail{}, false
269 }
270 detail := usage.Detail{
271 InputTokens: usageNode.Get("input_tokens").Int(),
272 OutputTokens: usageNode.Get("output_tokens").Int(),
273 TotalTokens: usageNode.Get("total_tokens").Int(),
274 }
275 if detail.TotalTokens == 0 {
276 detail.TotalTokens = detail.InputTokens + detail.OutputTokens
277 }
278 if cached := usageNode.Get("input_tokens_details.cached_tokens"); cached.Exists() {
279 detail.CachedTokens = cached.Int()
280 }
281 if reasoning := usageNode.Get("output_tokens_details.reasoning_tokens"); reasoning.Exists() {
282 detail.ReasoningTokens = reasoning.Int()
283 }
284 return detail, true
285}
286
239func parseClaudeUsage(data []byte) usage.Detail {287func parseClaudeUsage(data []byte) usage.Detail {
240 usageNode := gjson.ParseBytes(data).Get("usage")288 usageNode := gjson.ParseBytes(data).Get("usage")
241 if !usageNode.Exists() {289 if !usageNode.Exists() {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t