router-for-me/CLIProxyAPI · error

select Claude device ID: session ID is empty

Error message

select Claude device ID: session ID is empty

What it means

SelectDeviceID validates the Claude credential's device-ID pool and the conversation session ID before returning the device ID used for OAuth session binding. After normalizing the pool it trims the session ID and rejects an empty value, because Claude's API requires a non-empty session identifier to pin requests to a device. This is a pure input-validation error: nothing has touched the network yet when it fires.

Source

Thrown at internal/auth/claude/identity.go:274

		seen[deviceID] = struct{}{}
		deviceIDs = append(deviceIDs, deviceID)
	}

	if changed {
		metadata[ClaudeDeviceIDsMetadataKey] = append([]string(nil), deviceIDs...)
	}
	return append([]string(nil), deviceIDs...), changed, nil
}

// SelectDeviceID returns the credential's sole device ID after validating the conversation session.
func SelectDeviceID(deviceIDs []string, sessionID string) (string, error) {
	deviceIDs = NormalizeDeviceIDPool(deviceIDs)
	if len(deviceIDs) != ClaudeDevicePoolSize {
		return "", fmt.Errorf("select Claude device ID: device pool has %d entries, want %d", len(deviceIDs), ClaudeDevicePoolSize)
	}
	sessionID = strings.TrimSpace(sessionID)
	if sessionID == "" {
		return "", fmt.Errorf("select Claude device ID: session ID is empty")
	}
	return deviceIDs[0], nil
}

// ValidDeviceID reports whether a value matches Claude Code's lowercase 64-hex device format.
func ValidDeviceID(value string) bool {
	if len(value) != claudeDeviceIDByteSize*2 || value != strings.ToLower(value) {
		return false
	}
	decoded, errDecode := hex.DecodeString(value)
	return errDecode == nil && len(decoded) == claudeDeviceIDByteSize
}

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Ensure the caller passes a non-empty conversation/session ID before invoking SelectDeviceID (e.g. default it to the request's conversation ID or a generated UUID).
  2. Trim the session ID at the point of intake so whitespace-only values are caught earlier with a clearer message.
  3. If session binding is optional for your flow, skip the SelectDeviceID call when no session exists instead of passing an empty string.
  4. Check upstream request parsing to confirm the session field name still matches after schema changes.

Example fix

// before
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, req.SessionID)

// after
sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
    sessionID = req.ConversationID // or generate a UUID for anonymous use
}
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID)
Defensive patterns

Strategy: validation

Validate before calling

sessionID := strings.TrimSpace(req.SessionID)
if sessionID == "" {
    return fmt.Errorf("conversation requires a session ID")
}
deviceID, err := claude.SelectDeviceID(pool.DeviceIDs, sessionID)

Type guard

func hasSessionID(id string) bool { return strings.TrimSpace(id) != "" }

Prevention

When it happens

Trigger: Calling SelectDeviceID(deviceIDs, sessionID) where sessionID is "", consists only of whitespace, or was never populated (e.g. an empty conversation ID field in a request payload). It only fires after the pool itself already has exactly ClaudeDevicePoolSize entries, so the device pool is valid and the session ID is the sole problem.

Common situations: Passing a request's session/conversation ID straight from user input without defaulting it; refactoring a caller so the session ID variable is no longer set; testing with a hand-built credential and forgetting the session field; whitespace-padded session IDs copied from config files.

Related errors


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