exchangeDeviceCode attempts to exchange the device code for an access token. Returns (token, error, shouldContinue).
(ctx context.Context, deviceCode string)
| 264 | // exchangeDeviceCode attempts to exchange the device code for an access token. |
| 265 | // Returns (token, error, shouldContinue). |
| 266 | func (c *DeviceFlowClient) exchangeDeviceCode(ctx context.Context, deviceCode string) (*KimiTokenData, error, bool) { |
| 267 | data := url.Values{} |
| 268 | data.Set("client_id", kimiClientID) |
| 269 | data.Set("device_code", deviceCode) |
| 270 | data.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") |
| 271 | |
| 272 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, kimiTokenURL, strings.NewReader(data.Encode())) |
| 273 | if err != nil { |
| 274 | return nil, fmt.Errorf("kimi: failed to create token request: %w", err), false |
| 275 | } |
| 276 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
| 277 | req.Header.Set("Accept", "application/json") |
| 278 | for k, v := range c.commonHeaders() { |
| 279 | req.Header.Set(k, v) |
| 280 | } |
| 281 | |
| 282 | resp, err := c.httpClient.Do(req) |
| 283 | if err != nil { |
| 284 | return nil, fmt.Errorf("kimi: token request failed: %w", err), false |
| 285 | } |
| 286 | defer func() { |
| 287 | if errClose := resp.Body.Close(); errClose != nil { |
| 288 | log.Errorf("kimi token exchange: close body error: %v", errClose) |
| 289 | } |
| 290 | }() |
| 291 | |
| 292 | bodyBytes, err := io.ReadAll(resp.Body) |
| 293 | if err != nil { |
| 294 | return nil, fmt.Errorf("kimi: failed to read token response: %w", err), false |
| 295 | } |
| 296 | |
| 297 | // Parse response - Kimi returns 200 for both success and pending states |
| 298 | var oauthResp struct { |
| 299 | Error string `json:"error"` |
| 300 | ErrorDescription string `json:"error_description"` |
| 301 | AccessToken string `json:"access_token"` |
| 302 | RefreshToken string `json:"refresh_token"` |
| 303 | TokenType string `json:"token_type"` |
| 304 | ExpiresIn float64 `json:"expires_in"` |
| 305 | Scope string `json:"scope"` |
| 306 | } |
| 307 | |
| 308 | if err = json.Unmarshal(bodyBytes, &oauthResp); err != nil { |
| 309 | return nil, fmt.Errorf("kimi: failed to parse token response: %w", err), false |
| 310 | } |
| 311 | |
| 312 | if oauthResp.Error != "" { |
| 313 | switch oauthResp.Error { |
| 314 | case "authorization_pending": |
| 315 | return nil, nil, true // Continue polling |
| 316 | case "slow_down": |
| 317 | return nil, nil, true // Continue polling (with increased interval handled by caller) |
| 318 | case "expired_token": |
| 319 | return nil, fmt.Errorf("kimi: device code expired"), false |
| 320 | case "access_denied": |
| 321 | return nil, fmt.Errorf("kimi: access denied by user"), false |
| 322 | default: |
| 323 | return nil, fmt.Errorf("kimi: OAuth error: %s - %s", oauthResp.Error, oauthResp.ErrorDescription), false |
no test coverage detected