router-for-me/CLIProxyAPI · error

kimi: device code expired

Error message

kimi: device code expired

What it means

The local deadline computed in PollForToken (min of device code ExpiresIn and the 15-minute maxPollDuration) passed on a tick before the token endpoint returned success. The user simply did not authorize in time; the device code is now dead and a new one must be requested.

Source

Thrown at internal/auth/kimi/kimi.go:250

	deadline := time.Now().Add(maxPollDuration)
	if deviceCode.ExpiresIn > 0 {
		codeDeadline := time.Now().Add(time.Duration(deviceCode.ExpiresIn) * time.Second)
		if codeDeadline.Before(deadline) {
			deadline = codeDeadline
		}
	}

	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return nil, fmt.Errorf("kimi: context cancelled: %w", ctx.Err())
		case <-ticker.C:
			if time.Now().After(deadline) {
				return nil, fmt.Errorf("kimi: device code expired")
			}

			token, pollErr, shouldContinue := c.exchangeDeviceCode(ctx, deviceCode.DeviceCode)
			if token != nil {
				return token, nil
			}
			if !shouldContinue {
				return nil, pollErr
			}
			// Continue polling
		}
	}
}

// exchangeDeviceCode attempts to exchange the device code for an access token.
// Returns (token, error, shouldContinue).
func (c *DeviceFlowClient) exchangeDeviceCode(ctx context.Context, deviceCode string) (*KimiTokenData, error, bool) {
	data := url.Values{}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Restart the login flow to get a fresh device code, then authorize promptly at the printed verification URL
  2. Make sure the verification URL and user code are surfaced clearly to the user (the CLI prints them — don't miss them)
  3. Check container/VM clock sync (NTP) if expiry seems to fire far too early
Defensive patterns

Strategy: fallback

Validate before calling

// Surface the deadline to the user up front so they know the window
fmt.Printf("Authorize within %s at %s (code: %s)\n",
    time.Duration(deviceCode.ExpiresIn)*time.Second,
    deviceCode.VerificationURI, deviceCode.UserCode)

Type guard

func isDeviceCodeExpiredErr(err error) bool {
    return err != nil && strings.Contains(err.Error(), "kimi: device code expired")
}

Try / catch

if err != nil {
    if strings.Contains(err.Error(), "device code expired") {
        // restart flow automatically once
        deviceCode, _ = k.StartDeviceFlow(ctx)
        return k.WaitForAuthorization(ctx, deviceCode)
    }
    return err
}

Prevention

When it happens

Trigger: User never visits the verification URI or does not approve within deviceCode.ExpiresIn seconds; the flow started and was left idle past 15 minutes even if ExpiresIn claims longer; system clock skew making time.Now().After(deadline) trigger early.

Common situations: User starts ./cli-proxy-api login, walks away; verification URL buried in terminal output and never opened; slow_down polling backing things up so checks happen late; clock drift in VMs/containers.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/eb45ed0a0c931f39. Report an issue: GitHub.