diff --git a/cmd/copilot/main.go b/cmd/copilot/main.go index 047ca84561bb47c516336a1f0f3af664789f10d0..ba9a0741f6e8ede0996fa6a1cccd052e8af79674 100644 --- a/cmd/copilot/main.go +++ b/cmd/copilot/main.go @@ -23,6 +23,22 @@ type Response struct { Data []Model `json:"data"` } +type APITokenResponse struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` + Endpoints APITokenResponseEndpoints `json:"endpoints"` +} + +type APITokenResponseEndpoints struct { + API string `json:"api"` +} + +type APIToken struct { + APIKey string + ExpiresAt time.Time + APIEndpoint string +} + type Model struct { ID string `json:"id"` Name string `json:"name"` @@ -97,6 +113,7 @@ func run() error { if err != nil { return fmt.Errorf("unable to marshal json: %w", err) } + data = append(data, '\n') if err := os.WriteFile("internal/providers/configs/copilot.json", data, 0o600); err != nil { return fmt.Errorf("unable to write copilog.json: %w", err) } @@ -107,40 +124,85 @@ func fetchCopilotModels() ([]Model, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - req, err := http.NewRequestWithContext( - ctx, - "GET", - "https://api.githubcopilot.com/models", - nil, - ) + oauthToken := copilotToken() + if oauthToken == "" { + return nil, fmt.Errorf("no OAuth token available") + } + + // Step 1: Fetch API token from the token endpoint + tokenURL := "https://api.github.com/copilot_internal/v2/token" //nolint:gosec + tokenReq, err := http.NewRequestWithContext(ctx, "GET", tokenURL, nil) if err != nil { - return nil, fmt.Errorf("unable to create request: %w", err) + return nil, fmt.Errorf("unable to create token request: %w", err) } - req.Header.Set("Accept", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", copilotToken())) + tokenReq.Header.Set("Accept", "application/json") + tokenReq.Header.Set("Authorization", fmt.Sprintf("token %s", oauthToken)) + + // Use approved integration ID to bypass client check + tokenReq.Header.Set("Copilot-Integration-Id", "vscode-chat") + tokenReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1") client := &http.Client{} - resp, err := client.Do(req) + tokenResp, err := client.Do(tokenReq) + if err != nil { + return nil, fmt.Errorf("unable to make token request: %w", err) + } + defer tokenResp.Body.Close() //nolint:errcheck + + tokenBody, err := io.ReadAll(tokenResp.Body) if err != nil { - return nil, fmt.Errorf("unable to make http request: %w", err) + return nil, fmt.Errorf("unable to read token response body: %w", err) } - defer resp.Body.Close() //nolint:errcheck - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + if tokenResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code from token endpoint: %d", tokenResp.StatusCode) } - bts, err := io.ReadAll(resp.Body) + var tokenData APITokenResponse + if err := json.Unmarshal(tokenBody, &tokenData); err != nil { + return nil, fmt.Errorf("unable to unmarshal token response: %w", err) + } + + // Convert to APIToken + expiresAt := time.Unix(tokenData.ExpiresAt, 0) + apiToken := APIToken{ + APIKey: tokenData.Token, + ExpiresAt: expiresAt, + APIEndpoint: tokenData.Endpoints.API, + } + + // Step 2: Use the dynamic endpoint from the token to fetch models + modelsURL := apiToken.APIEndpoint + "/models" + modelsReq, err := http.NewRequestWithContext(ctx, "GET", modelsURL, nil) + if err != nil { + return nil, fmt.Errorf("unable to create models request: %w", err) + } + modelsReq.Header.Set("Accept", "application/json") + modelsReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiToken.APIKey)) + modelsReq.Header.Set("Copilot-Integration-Id", "vscode-chat") + modelsReq.Header.Set("User-Agent", "GitHubCopilotChat/0.1") + + modelsResp, err := client.Do(modelsReq) + if err != nil { + return nil, fmt.Errorf("unable to make models request: %w", err) + } + defer modelsResp.Body.Close() //nolint:errcheck + + modelsBody, err := io.ReadAll(modelsResp.Body) if err != nil { - return nil, fmt.Errorf("unable to read response body: %w", err) + return nil, fmt.Errorf("unable to read models response body: %w", err) + } + + if modelsResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status code from models endpoint: %d", modelsResp.StatusCode) } // for debugging _ = os.MkdirAll("tmp", 0o700) - _ = os.WriteFile("tmp/copilot-response.json", bts, 0o600) + _ = os.WriteFile("tmp/copilot-response.json", modelsBody, 0o600) var data Response - if err := json.Unmarshal(bts, &data); err != nil { + if err := json.Unmarshal(modelsBody, &data); err != nil { return nil, fmt.Errorf("unable to unmarshal json: %w", err) } return data.Data, nil diff --git a/internal/providers/configs/copilot.json b/internal/providers/configs/copilot.json index a1436d50a39bcb63284e1ab962ee1948343eaa3c..b8b1e2d3fb333d8db0ab1717c75205242c7448c1 100644 --- a/internal/providers/configs/copilot.json +++ b/internal/providers/configs/copilot.json @@ -13,7 +13,7 @@ "cost_per_1m_out": 0, "cost_per_1m_in_cached": 0, "cost_per_1m_out_cached": 0, - "context_window": 144000, + "context_window": 200000, "default_max_tokens": 16000, "can_reason": false, "supports_attachments": true, @@ -26,7 +26,7 @@ "cost_per_1m_out": 0, "cost_per_1m_in_cached": 0, "cost_per_1m_out_cached": 0, - "context_window": 160000, + "context_window": 200000, "default_max_tokens": 16000, "can_reason": false, "supports_attachments": true, @@ -52,7 +52,7 @@ "cost_per_1m_out": 0, "cost_per_1m_in_cached": 0, "cost_per_1m_out_cached": 0, - "context_window": 144000, + "context_window": 200000, "default_max_tokens": 16000, "can_reason": false, "supports_attachments": true, @@ -71,6 +71,32 @@ "supports_attachments": true, "options": {} }, + { + "id": "gemini-3-flash-preview", + "name": "Gemini 3 Flash (Preview)", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 128000, + "default_max_tokens": 64000, + "can_reason": false, + "supports_attachments": true, + "options": {} + }, + { + "id": "gemini-3-pro-preview", + "name": "Gemini 3 Pro (Preview)", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 128000, + "default_max_tokens": 64000, + "can_reason": false, + "supports_attachments": true, + "options": {} + }, { "id": "gpt-3.5-turbo-0613", "name": "GPT 3.5 Turbo", @@ -84,6 +110,19 @@ "supports_attachments": false, "options": {} }, + { + "id": "gpt-4-0125-preview", + "name": "GPT 4 Turbo", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 128000, + "default_max_tokens": 4096, + "can_reason": false, + "supports_attachments": false, + "options": {} + }, { "id": "gpt-4-0613", "name": "GPT 4", @@ -110,6 +149,19 @@ "supports_attachments": true, "options": {} }, + { + "id": "gpt-5-codex", + "name": "GPT-5-Codex (Preview)", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 400000, + "default_max_tokens": 128000, + "can_reason": false, + "supports_attachments": true, + "options": {} + }, { "id": "gpt-5-mini", "name": "GPT-5 mini", @@ -162,6 +214,19 @@ "supports_attachments": true, "options": {} }, + { + "id": "gpt-5.1-codex-mini", + "name": "GPT-5.1-Codex-Mini", + "cost_per_1m_in": 0, + "cost_per_1m_out": 0, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 400000, + "default_max_tokens": 128000, + "can_reason": false, + "supports_attachments": true, + "options": {} + }, { "id": "gpt-5.2", "name": "GPT-5.2",